<?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[Chris' Blog - Kubernetes, Platform Engineering, and DevOps]]></title><description><![CDATA[Platform engineering notes, Kubernetes and DevOps projects, developer tools, architecture diagrams, and browser experiments by Chris House.]]></description><link>https://chrishouse.io</link><generator>GatsbyJS</generator><lastBuildDate>Fri, 11 Sep 2026 17:08:17 GMT</lastBuildDate><item><title><![CDATA[Connecting My Local AI to GrowthBook with MCP (Part 6)]]></title><description><![CDATA[Part 6 of the self-hosting series: connecting Ollama chat to GrowthBook through a Docker MCP server, with flag evaluation, Cloudflare Tunnel routing, and tool calls in Pages Functions.]]></description><link>https://chrishouse.io/local-ai-docker-mcp-growthbook/</link><guid isPermaLink="false">https://chrishouse.io/local-ai-docker-mcp-growthbook/</guid><pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I use GrowthBook feature flags for the site&apos;s interactive demos. One flag selects a scene, another controls an effect, and targeting rules can change what a particular visitor sees. I wanted to ask the site&apos;s chat a question like “What is &lt;code class=&quot;language-text&quot;&gt;dotc-fx&lt;/code&gt; set to?” and have it look up the answer.&lt;/p&gt;
&lt;p&gt;The model was already running locally through Ollama, with the &lt;a href=&quot;/self-hosted-llm-cloudflare-tunnel-ollama&quot;&gt;Cloudflare Tunnel from Part 3&lt;/a&gt; connecting it to the site. I added a Docker service that exposes GrowthBook reads through Model Context Protocol, then wired the chat&apos;s Pages Functions to call it.&lt;/p&gt;
&lt;p&gt;The first flag lookup returned &lt;code class=&quot;language-text&quot;&gt;FIRE&lt;/code&gt;. Getting that value into a normal chat answer involved flag evaluation, two routes through the same tunnel, and a couple of differences between testing in Node and running in Cloudflare Pages.&lt;/p&gt;
&lt;h2 id=&quot;how-the-request-travels&quot;&gt;How the Request Travels&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/learn/architecture&quot;&gt;Model Context Protocol&lt;/a&gt;, or MCP, lets an application discover a server&apos;s tools and call them using defined input schemas. My server offers operations for listing permitted flags and evaluating a flag by key.&lt;/p&gt;
&lt;p&gt;The Pages Function acts as the MCP client. It discovers those tools and includes their definitions in the request to Ollama. When the model asks for a flag lookup, Pages calls the MCP server, adds the result to the conversation, and asks the model to produce the answer.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Browser: Neon GPT or Station Intelligence
                 |
                 v
Cloudflare Pages: /api/llm or /api/ask
       |                           |
       v                           v
llm.chrishouse.io/v1     llm.chrishouse.io/mcp
       |                           |
       +---- Cloudflare Access ----+
                     |
           Existing Cloudflare Tunnel
                     |
              Desktop at home
              /             \
      Ollama :11434      Docker MCP :8792
      Qwen model               |
                               v
                      GrowthBook SDK endpoint&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The site&apos;s article search still supplies background from posts, and &lt;a href=&quot;/redis-agent-memory-server-ollama&quot;&gt;Redis memory&lt;/a&gt; supplies conversation context. The flag tool adds a lookup at the time of the question. That matters for a value I can change in GrowthBook after publishing an article about it.&lt;/p&gt;
&lt;h2 id=&quot;building-the-mcp-server&quot;&gt;Building the MCP Server&lt;/h2&gt;
&lt;p&gt;I used Microsoft&apos;s &lt;a href=&quot;https://github.com/microsoft/skills/tree/main/.github/skills/mcp-builder&quot;&gt;MCP builder agent skill&lt;/a&gt; as a guide for the tool definitions, validation, and transport. The service is a TypeScript application under &lt;code class=&quot;language-text&quot;&gt;services/home-mcp/&lt;/code&gt; in &lt;a href=&quot;https://github.com/crh225/theblog&quot;&gt;the blog repository&lt;/a&gt;, with its own dependencies, tests, and Dockerfile.&lt;/p&gt;
&lt;p&gt;It uses the &lt;a href=&quot;https://github.com/modelcontextprotocol/typescript-sdk/tree/v1.x&quot;&gt;MCP TypeScript SDK&apos;s v1.x line&lt;/a&gt; and &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25/basic/transports&quot;&gt;Streamable HTTP&lt;/a&gt;. Each request gets a fresh server and transport instance, with JSON responses. That gives the Pages Function an HTTP endpoint it can call with &lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The server registers three read operations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;growthbook_list_flags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A paginated list of permitted flag keys and their evaluated values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;growthbook_get_flag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The evaluated value of one permitted flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;house_get_capabilities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The service&apos;s configured integrations and availability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each tool has a Zod input schema and an output schema. Results include structured JSON and a text representation. Both the server and the Pages client enforce allowlists: the server limits which flags can be read, and Pages limits which tool names it will execute.&lt;/p&gt;
&lt;p&gt;The GrowthBook connection uses the public SDK payload and allows ten existing &lt;code class=&quot;language-text&quot;&gt;dotc-*&lt;/code&gt; flags. The service only needs read access to the configuration used by those demos.&lt;/p&gt;
&lt;h2 id=&quot;evaluating-a-flag&quot;&gt;Evaluating a Flag&lt;/h2&gt;
&lt;p&gt;A GrowthBook flag can have targeting rules and experiment assignments, so returning its &lt;code class=&quot;language-text&quot;&gt;defaultValue&lt;/code&gt; would miss part of the configuration. I pass the payload and attributes through the &lt;a href=&quot;https://docs.growthbook.io/lib/node&quot;&gt;GrowthBook SDK evaluator&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The default evaluation context is:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;station-ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;plan&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;free&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The tools also accept &lt;code class=&quot;language-text&quot;&gt;id&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;plan&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;dotId&lt;/code&gt; attributes. For example, the color demo targets by &lt;code class=&quot;language-text&quot;&gt;dotId&lt;/code&gt;. To investigate what a particular dot sees, the lookup needs that dot&apos;s identifier. The default context gives a consistent answer when the question supplies only a flag key.&lt;/p&gt;
&lt;p&gt;This abbreviated response came from a local call using the real GrowthBook SDK endpoint:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotc-fx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;FIRE&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;on&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;force&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;attributes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;station-ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;plan&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;free&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The attributes and &lt;code class=&quot;language-text&quot;&gt;source&lt;/code&gt; help explain the returned value. They also make it possible to compare evaluations for different targeting contexts in the &lt;a href=&quot;/tools/dotc-flags/&quot;&gt;Flag Lab&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The full response includes &lt;code class=&quot;language-text&quot;&gt;fetched_at&lt;/code&gt;, when the service fetched the payload, and &lt;code class=&quot;language-text&quot;&gt;source_updated_at&lt;/code&gt;, the timestamp supplied by GrowthBook. The service caches payloads for fifteen seconds. After that, a failed refresh produces an error. Missing flags and keys outside the allowlist also produce errors, while a flag evaluated to &lt;code class=&quot;language-text&quot;&gt;false&lt;/code&gt; remains a successful lookup.&lt;/p&gt;
&lt;h2 id=&quot;running-the-service-in-docker&quot;&gt;Running the Service in Docker&lt;/h2&gt;
&lt;p&gt;The Compose definition is in &lt;code class=&quot;language-text&quot;&gt;docker-compose.mcp.yml&lt;/code&gt;. These are the relevant settings:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;home-mcp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./services/home&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mcp
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chrishouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;home&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mcp
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;env_file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .env.home&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mcp
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;127.0.0.1:8792:8792&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;read_only&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cap_drop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ALL&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;security_opt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;privileges&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Docker image compiles the TypeScript and runs the application as the non-root &lt;code class=&quot;language-text&quot;&gt;node&lt;/code&gt; user. Docker publishes port 8792 on the host&apos;s loopback interface, where the local cloudflared service can reach it. The container has no host filesystem or Docker socket mounted.&lt;/p&gt;
&lt;p&gt;From the repository root:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;node scripts/configure-home-mcp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mjs
docker compose &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;f docker-compose&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mcp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;yml up &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;build &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;d
npm &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;prefix services/home-mcp ci
npm &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;prefix services/home-mcp run verify&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The setup helper creates a random bearer token in the ignored &lt;code class=&quot;language-text&quot;&gt;.env.home-mcp&lt;/code&gt; file and writes the matching token and local URL to &lt;code class=&quot;language-text&quot;&gt;.dev.vars&lt;/code&gt;. The verification command checks the running MCP service.&lt;/p&gt;
&lt;p&gt;For local development, Gatsby serves the frontend on port 8000 and the Pages runtime serves the API on 8788. Restart the Pages process after changing &lt;code class=&quot;language-text&quot;&gt;.dev.vars&lt;/code&gt; so it loads the new credentials.&lt;/p&gt;
&lt;h2 id=&quot;reusing-the-existing-tunnel&quot;&gt;Reusing the Existing Tunnel&lt;/h2&gt;
&lt;p&gt;Ollama already had a route at &lt;code class=&quot;language-text&quot;&gt;llm.chrishouse.io/v1&lt;/code&gt;. I added &lt;code class=&quot;language-text&quot;&gt;/mcp&lt;/code&gt; to the same hostname and pointed it at the Docker service. The relevant ingress rules are:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; llm.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ^/mcp/&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;$
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//127.0.0.1&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8792&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;originRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;httpHostHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8792&lt;/span&gt;

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; llm.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ^/v1/.*
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11434&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;originRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;httpHostHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11434&lt;/span&gt;

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; llm.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http_status&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;# Other hostname rules remain above the final catch-all.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http_status&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Cloudflared matches rules in order. The &lt;code class=&quot;language-text&quot;&gt;/mcp&lt;/code&gt; rule belongs above the hostname&apos;s 403 rule. This is an excerpt from a shared tunnel configuration; its other hostname routes stay in place.&lt;/p&gt;
&lt;p&gt;I validated the configuration and restarted the Windows service to load it. The commands, run from Administrator PowerShell, are:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;cloudflared tunnel &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;config &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:USERPROFILE\.cloudflared\config.yml&quot;&lt;/span&gt; ingress validate
&lt;span class=&quot;token function&quot;&gt;Restart-Service&lt;/span&gt; Cloudflared&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pages uses &lt;code class=&quot;language-text&quot;&gt;HOME_MCP_URL&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;HOME_MCP_TOKEN&lt;/code&gt; for the MCP connection. The repository&apos;s helper can upload those two secrets to the site&apos;s Pages project:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;node scripts/configure-home-mcp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mjs &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;cloudflare&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Cloudflare Access supplies the outer authentication layer. Because MCP and Ollama share an origin here, the Pages client reuses the existing Ollama Access service credentials. The MCP server also checks its own bearer token, validates the host, and rejects browser &lt;code class=&quot;language-text&quot;&gt;Origin&lt;/code&gt; headers. Both sets of credentials stay in Pages Functions; the browser sends its chat requests to the site&apos;s API.&lt;/p&gt;
&lt;h2 id=&quot;connecting-the-chat&quot;&gt;Connecting the Chat&lt;/h2&gt;
&lt;p&gt;Neon GPT enables the tool flow with &lt;code class=&quot;language-text&quot;&gt;home_tools: true&lt;/code&gt; in its request to &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt;. Station Intelligence uses the same integration in &lt;code class=&quot;language-text&quot;&gt;/api/ask&lt;/code&gt;, alongside article retrieval.&lt;/p&gt;
&lt;p&gt;The tool loop lives in &lt;code class=&quot;language-text&quot;&gt;functions/api/lib/home-mcp.js&lt;/code&gt;. It initializes MCP, discovers permitted tools, and translates their schemas into the tool definitions used by the chat completion API. A requested tool call goes through the client&apos;s allowlist before execution. Its result then becomes another message in the model&apos;s conversation.&lt;/p&gt;
&lt;p&gt;I limited the loop to three calls per round and two rounds before the final answer. Individual MCP requests have an eight-second timeout, with a two-minute deadline around the completion flow. If the service is unavailable, the model receives that failure so it can explain why a current flag value could not be retrieved.&lt;/p&gt;
&lt;h3 id=&quot;qwen-sometimes-returns-the-tool-request-as-text&quot;&gt;Qwen Sometimes Returns the Tool Request as Text&lt;/h3&gt;
&lt;p&gt;During testing, &lt;code class=&quot;language-text&quot;&gt;qwen2.5-coder:14B&lt;/code&gt; sometimes put this JSON in the assistant&apos;s text instead of the API&apos;s &lt;code class=&quot;language-text&quot;&gt;tool_calls&lt;/code&gt; field:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;growthbook_get_flag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;arguments&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotc-fx&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The original handler displayed the JSON and never performed the lookup. I added support for a whole-message JSON tool request whose name matches an offered tool. It follows the same validation and execution path as a native tool call. Requiring the entire message to match keeps ordinary prose with embedded JSON examples out of that path.&lt;/p&gt;
&lt;h3 id=&quot;pages-exposed-a-fetch-difference&quot;&gt;Pages Exposed a Fetch Difference&lt;/h3&gt;
&lt;p&gt;The MCP client initially used &lt;code class=&quot;language-text&quot;&gt;redirect: &quot;error&quot;&lt;/code&gt;. Its Node test passed, but the Pages runtime rejected that fetch option. Switching to &lt;code class=&quot;language-text&quot;&gt;redirect: &quot;manual&quot;&lt;/code&gt; and explicitly rejecting redirects fixed the request in Pages.&lt;/p&gt;
&lt;p&gt;This also handles an Access login redirect as a connection failure. The MCP client stops at that response instead of following it with the bearer token. Sending a request through the local Pages runtime caught the issue that the Node test had missed.&lt;/p&gt;
&lt;h2 id=&quot;a-flag-lookup-through-the-chat-api&quot;&gt;A Flag Lookup Through the Chat API&lt;/h2&gt;
&lt;p&gt;With Docker, Ollama, and the local Pages runtime running, a request to &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt; returned this abbreviated response:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;The current value of the `dotc-scene` flag is `\&quot;fx\&quot;`.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;backend&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;house&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;home_tools&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;connected&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;calls&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;growthbook_get_flag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;ok&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;home_tools.calls&lt;/code&gt; entry records the successful lookup alongside the answer. A request through Station Intelligence&apos;s &lt;code class=&quot;language-text&quot;&gt;/api/ask&lt;/code&gt; also called &lt;code class=&quot;language-text&quot;&gt;growthbook_get_flag&lt;/code&gt; and returned &lt;code class=&quot;language-text&quot;&gt;FIRE&lt;/code&gt; for &lt;code class=&quot;language-text&quot;&gt;dotc-fx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The repository includes a chat verification script for exercising the integration with Ollama:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;node services/home-mcp/scripts/verify-chat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mjs &lt;span class=&quot;token string&quot;&gt;&quot;Read the current dotc-fx flag using your tool.&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For a flag question, I can now trace the answer through the tool call to its evaluated value and attributes. If the result differs from what a demo displays, those attributes are the first place to look: the tool&apos;s default &lt;code class=&quot;language-text&quot;&gt;station-ai&lt;/code&gt; context and the visitor&apos;s context can match different GrowthBook rules.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[One Hostname, Two Origins: Cloudflare Load Balancer Cutover]]></title><description><![CDATA[Moving a platform onto new infrastructure without changing the hostname everyone already uses. I built the two-origin version on my own domain first, with Cloudflare Load Balancing, two Docker containers and a pair of tunnels, so the cutover is a config change at the edge that reverses in seconds.]]></description><link>https://chrishouse.io/cloudflare-load-balancer-hostname-cutover/</link><guid isPermaLink="false">https://chrishouse.io/cloudflare-load-balancer-hostname-cutover/</guid><pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We have a platform at work living on two sets of infrastructure at once. There is the original one, which everybody knows the name of and half the internal tooling has hardcoded. Then there is the new one, which got stood up beside it with the word &lt;code class=&quot;language-text&quot;&gt;prod&lt;/code&gt; wedged into its hostname because at the time nobody had a better idea. Nobody likes that name. The plan is that the original hostname survives, the new infrastructure ends up underneath it, and &lt;code class=&quot;language-text&quot;&gt;prod&lt;/code&gt; quietly disappears from anything a human ever types.&lt;/p&gt;
&lt;p&gt;Most of the work here is deciding what a hostname is allowed to mean, and who gets to depend on it. The load balancer is just what lets me change the answer without changing the name. So before touching anything that matters, I built the whole shape on my own domain, where the blast radius is me.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;/h2&gt;
&lt;p&gt;Three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;service.chrishouse.io&lt;/code&gt; is the only name anyone sees, permanently. Whatever serves it can change underneath without anybody noticing.&lt;/li&gt;
&lt;li&gt;Behind it, two independent origins, &lt;code class=&quot;language-text&quot;&gt;service-east&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;service-west&lt;/code&gt;, standing in for old infrastructure and new.&lt;/li&gt;
&lt;li&gt;Moving production from one to the other is one deliberate action, it takes seconds, and it reverses just as fast.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The thing I explicitly did not want is a percentage ramp. Ten percent, then fifty, then ninety sounds sophisticated, but it only means anything if both backends are genuinely interchangeable to a user: shared database, no diverging session state. If they are not, a 50/50 split is two different worlds served at random. What I wanted was a switch. This side is live now, with an undo button next to it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-building-blocks&quot;&gt;The Building Blocks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/load-balancing/&quot;&gt;Cloudflare Load Balancing&lt;/a&gt;&lt;/strong&gt; is the steering layer. It runs about $5 a month at the entry tier with two origins included, which is exactly two origins more than I need to prove the point. The object model is small: a load balancer attached to a hostname, holding an ordered list of &lt;strong&gt;pools&lt;/strong&gt;, each pool holding &lt;strong&gt;endpoints&lt;/strong&gt;, with a &lt;strong&gt;monitor&lt;/strong&gt; health-checking them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&quot;&gt;Cloudflare Tunnel&lt;/a&gt;&lt;/strong&gt; connects the origins. Both of mine are containers on the desktop under my desk. No port forwarding, no static IP, no inbound anything. Cloudflare&apos;s own guidance is one tunnel per data centre and one pool per tunnel, which maps onto east and west without requiring any creativity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two nginx containers&lt;/strong&gt; are the origins, standing in for the two application stacks. They do no routing and no load balancing of their own. All of that happens at the edge, and these are just the things at the far end that answer. Identical config, different environment variables, so the only honest difference between them is the label each one prints about itself. If the two sides were not interchangeable, the demo would be lying.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;x-origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token important&quot;&gt;&amp;amp;origin&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;1.27&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
  &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./nginx/default.conf.template&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/etc/nginx/templates/default.conf.template&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;ro

&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;east&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token important&quot;&gt;*origin&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;REGION&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; east
      &lt;span class=&quot;token key atrule&quot;&gt;STACK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; legacy
      &lt;span class=&quot;token key atrule&quot;&gt;RELEASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;8081:80&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;west&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token important&quot;&gt;*origin&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;REGION&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; west
      &lt;span class=&quot;token key atrule&quot;&gt;STACK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; new
      &lt;span class=&quot;token key atrule&quot;&gt;RELEASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v2
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;8082:80&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is no application behind either one. They exist to answer a request and admit which side they are, and that admission is four lines of nginx config. This is a stub, not something you would ever put in a real config:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;nginx&quot;&gt;&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; = /api/whoami&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;default_type&lt;/span&gt; application/json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{&quot;region&quot;:&quot;&lt;span class=&quot;token variable&quot;&gt;${REGION}&lt;/span&gt;&quot;,&quot;stack&quot;:&quot;&lt;span class=&quot;token variable&quot;&gt;${STACK}&lt;/span&gt;&quot;,&quot;hostSeen&quot;:&quot;&lt;span class=&quot;token variable&quot;&gt;$host&lt;/span&gt;&quot;}&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;${REGION}&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;${STACK}&lt;/code&gt; get substituted once at container start by the nginx image&apos;s envsubst entrypoint, so they say what the container &lt;em&gt;is&lt;/em&gt;. &lt;code class=&quot;language-text&quot;&gt;$host&lt;/code&gt; is an nginx runtime variable evaluated per request, so it says what the container was &lt;em&gt;asked for&lt;/em&gt;. Those two being able to disagree is the entire point.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;                  service.chrishouse.io
                           |
                  Cloudflare Load Balancer
                  steering: off (failover order)
                           |
              +------------+------------+
              v                         v
        pool: east                pool: west
        (live)                    (standby)
              |                         |
     &amp;lt;uuid&gt;.cfargotunnel.com   &amp;lt;uuid&gt;.cfargotunnel.com
              |                         |
        cloudflared               cloudflared
              |                         |
        nginx :80                 nginx :80
        legacy / v1               new / v2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The one setting that makes this work is &lt;strong&gt;traffic steering set to &lt;code class=&quot;language-text&quot;&gt;Off&lt;/code&gt;&lt;/strong&gt;. Cloudflare offers dynamic, geo, proximity and least-outstanding-requests steering, and every one of them hands the routing decision to a latency measurement. &lt;code class=&quot;language-text&quot;&gt;Off&lt;/code&gt; does something better. It uses the pool list &lt;em&gt;in order&lt;/em&gt;, and the first healthy pool takes everything. The order of that list is the switch.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-host-header&quot;&gt;The Host Header&lt;/h2&gt;
&lt;p&gt;Each endpoint in a pool carries a &lt;strong&gt;host header&lt;/strong&gt;, and what you put there decides the entire character of the migration.&lt;/p&gt;
&lt;p&gt;Set it to the origin&apos;s own name and you get &lt;strong&gt;override mode&lt;/strong&gt;. The visitor types &lt;code class=&quot;language-text&quot;&gt;service.chrishouse.io&lt;/code&gt;, the backend receives &lt;code class=&quot;language-text&quot;&gt;service-east.chrishouse.io&lt;/code&gt;. It works, and it is the easy option, because the backend needs to know nothing.&lt;/p&gt;
&lt;p&gt;The trouble is what a backend does with the hostname it thinks it has. A 302, a cookie &lt;code class=&quot;language-text&quot;&gt;Domain&lt;/code&gt; attribute, an OIDC &lt;code class=&quot;language-text&quot;&gt;redirect_uri&lt;/code&gt;, a link in a notification email. All of them get built from the name the app believes it is serving. In override mode an app will happily hand your users the internal hostname, which is the name you were trying to retire in the first place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preserve mode&lt;/strong&gt; is the other option. Put the &lt;em&gt;public&lt;/em&gt; name in the host header on every pool, and the backend sees &lt;code class=&quot;language-text&quot;&gt;service.chrishouse.io&lt;/code&gt; no matter which side is serving. It never learns it moved.&lt;/p&gt;
&lt;p&gt;That costs something. Every backend has to actually answer to the final hostname before you can point the final hostname at it, and how much work that is depends on how the backend is reached.&lt;/p&gt;
&lt;p&gt;A backend exposed as a public HTTPS origin needs two things per app: a routing rule for the hostname, and that hostname on the certificate. On Kubernetes that is a &lt;code class=&quot;language-text&quot;&gt;host:&lt;/code&gt; rule on the Ingress plus the name on the cert-manager SAN list, or &lt;code class=&quot;language-text&quot;&gt;hostnames:&lt;/code&gt; on the HTTPRoute if you are on Gateway API, where the parent Gateway&apos;s listener also has to permit it.&lt;/p&gt;
&lt;p&gt;A backend behind a Cloudflare tunnel only needs the routing half. Cloudflare terminates TLS at the edge, the tunnel is encrypted, and cloudflared talks plain HTTP to the service inside the cluster, so there is no public certificate to issue for that hostname at all. Cert-manager drops out of the critical path entirely, which is a smaller change than it first looks like.&lt;/p&gt;
&lt;p&gt;What you do need is a rule on the tunnel:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;service.chrishouse.io       -&gt; http://east:80
service-east.chrishouse.io  -&gt; http://east:80
(any host)                  -&gt; http_status:404&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Worth knowing: the Zero Trust dashboard will not let you add a rule without a hostname, because in that UI a route &lt;em&gt;is&lt;/em&gt; a published hostname and it creates a DNS record to match. The catch-all slot is already occupied by the terminating &lt;code class=&quot;language-text&quot;&gt;http_status:404&lt;/code&gt; that cloudflared requires. Extra rules go in through the API. I would use the API anyway. Naming the hostname explicitly is the move you will make on every real app, so the rehearsal may as well practise it rather than take a wildcard shortcut.&lt;/p&gt;
&lt;p&gt;The proof is one field. Here is the origin describing itself, through the load balancer, in preserve mode:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;region&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;west&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;stack&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;new&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;release&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;v2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;hostSeen&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;service.chrishouse.io&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;hostSeen&lt;/code&gt; is &lt;code class=&quot;language-text&quot;&gt;$host&lt;/code&gt; straight out of nginx, whatever the container was actually asked for. When that reads as the public name, the abstraction is real. When it reads as the origin&apos;s own name, you have a leak waiting to surface in somebody&apos;s password reset email.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-cutover&quot;&gt;The Cutover&lt;/h2&gt;
&lt;p&gt;With steering off, promoting a side means reordering a list. Having that as a script rather than a click path is what makes it something other people can run:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;PATCH&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/zones/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;zone&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/load_balancers/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;lb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;default_pools&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;idOf&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; idOf&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;standby&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;fallback_pool&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idOf&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;$ node cutover.js west
cut over: east -&gt; west   (fallback now west)

$ node cutover.js
live:     west
order:    west -&gt; east
fallback: west&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Under eight seconds from command to the new side serving, and &lt;code class=&quot;language-text&quot;&gt;node cutover.js east&lt;/code&gt; puts it back just as quickly. No DNS record changes, so there is no TTL to wait out and no client cache holding a stale answer. The decision happens at Cloudflare&apos;s edge on every request, which is the entire argument for a load balancer over swapping a CNAME.&lt;/p&gt;
&lt;p&gt;Note the fallback pool moving along with the primary. The fallback is where traffic goes when &lt;em&gt;everything&lt;/em&gt; reads unhealthy, and by far the likeliest cause of that is a monitor you misconfigured, not two independently dead stacks. It should always point at whichever side you currently trust most, which means it changes when the primary changes. Doing that by hand is a step people forget until an incident reminds them.&lt;/p&gt;
&lt;p&gt;A script in a runbook still assumes somebody finds the runbook, holds an API token, and types the right argument at 2am. The version I actually want is this wrapped as a &lt;a href=&quot;/golden-paths-day2-operations&quot;&gt;Day-2 action&lt;/a&gt; in Port, sitting on the service entity next to everything else you can do to it. The input is a dropdown with two values. The backend runs the same &lt;code class=&quot;language-text&quot;&gt;cutover.js&lt;/code&gt; from a workflow, so what the portal triggers is the same thing I would run by hand. What the portal adds is not the logic, it is the guardrails: who is allowed to press it, and what gets written down when they do. Moving production between two sets of infrastructure is exactly the kind of operation that should leave a record of who did it and when, and a terminal on my laptop leaves none.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-the-hostname-already-exists&quot;&gt;When the Hostname Already Exists&lt;/h2&gt;
&lt;p&gt;The demo had it easy. I invented &lt;code class=&quot;language-text&quot;&gt;service.chrishouse.io&lt;/code&gt; and made it a load balancer from nothing. The real migration does not get that. The hostname already exists, already has a DNS record, and already has users on it right now. A Cloudflare load balancer owns a hostname, so the record and the load balancer cannot both have it.&lt;/p&gt;
&lt;p&gt;The way through is to make the takeover a no-op. Point the load balancer&apos;s first pool at exactly what the DNS record already points at. Same origin, same host header, same response. The hostname changes what kind of object it is, and nothing else about the request path moves at all. Nothing has migrated yet. All you have changed is who decides where traffic goes, which is the thing you need in place before you can migrate anything.&lt;/p&gt;
&lt;p&gt;How risky that moment is comes down to one question: is the hostname proxied today?&lt;/p&gt;
&lt;p&gt;If it is already orange-clouded, this is close to free. Clients resolve to Cloudflare anycast addresses now and will resolve to Cloudflare anycast addresses afterwards. Nothing a client can observe changes, and there is no TTL to wait out. If it is grey-clouded and pointing straight at an ingress IP, clients would move from your address to Cloudflare&apos;s, which is full propagation exposure with a slow rollback. That case deserves its own change on its own timeline: lower the TTL, orange-cloud it, let it settle for a week, and only then treat the load balancer as a separate step.&lt;/p&gt;
&lt;p&gt;The sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save exactly what the record is today. Type, content, proxied flag, TTL. That is your rollback and there is no other copy of it.&lt;/li&gt;
&lt;li&gt;Get an origin address for legacy. If the record is a CNAME, its target already works. If it is an A record, the IP does. A DNS-only alias like &lt;code class=&quot;language-text&quot;&gt;myapp-legacy&lt;/code&gt; keeps the pool config readable and lets you repoint legacy later without touching the pool.&lt;/li&gt;
&lt;li&gt;Build the pool in preserve mode from the start, with the public hostname in the host header. This is the good part. Legacy already answers to that name and its certificate already covers it, so legacy needs no changes whatsoever. Every bit of work lands on the new cluster.&lt;/li&gt;
&lt;li&gt;Create the pool and monitor with no load balancer attached, and confirm the pool reads healthy. Pools are account-level objects, so you can validate the health check before anything is load-bearing.&lt;/li&gt;
&lt;li&gt;Rehearse on a parallel hostname. Pools are reusable across load balancers, so a throwaway name gives you a full dress rehearsal against the real origins.&lt;/li&gt;
&lt;li&gt;Create the real load balancer with only the legacy pool in it.&lt;/li&gt;
&lt;li&gt;Leave it alone for days. Nothing has migrated.&lt;/li&gt;
&lt;li&gt;Then put the public hostname on the new cluster&apos;s routing rules, and on its certificate too if it is reached as a public origin rather than through a tunnel. Add it as a second pool on standby, and reorder when you are ready.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps 1 through 5 change nothing. Step 8 reverses in seconds. Step 6 is the only one-way door, and only in the sense that undoing it means recreating a DNS record by hand instead of reordering a list. That is what step 1 is for.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;design-decisions&quot;&gt;Design Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why failover order instead of weights?&lt;/strong&gt; Because a weighted ramp is a promise about equivalence I could not honestly make for the real migration. Two backends only blend if they share state. Failover order gives me one live side and one standby, which is what I actually wanted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why tunnels instead of public origins?&lt;/strong&gt; Both origins are containers on a desktop in my house. A tunnel means no open ports, no static IP, and an origin address that only Cloudflare can route to. It also matters that the endpoint has to be the &lt;code class=&quot;language-text&quot;&gt;&amp;lt;uuid&gt;.cfargotunnel.com&lt;/code&gt; hostname directly. A friendlier CNAME pointing at it is explicitly unsupported, which is a fun twenty minutes if you assume otherwise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why one monitor for both pools?&lt;/strong&gt; A monitor defines how to check. The address and host header come from each endpoint, so one monitor means both sides are judged by an identical standard, which is the only way a comparison between them means anything. It also halves the health-check volume you are billed for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why adaptive routing on?&lt;/strong&gt; Failover across pools lets a request move to a healthy pool immediately instead of waiting for the monitor to notice. With a 60 second check interval, that is the difference between a minute of errors and none.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not just change DNS?&lt;/strong&gt; Because rollback is the whole point. A DNS change propagates at the mercy of every resolver and browser between you and your users, so the undo button takes as long as the button did. Edge-side steering reverses in seconds, and that is what lets you attempt a cutover during business hours instead of at midnight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not put the routing in nginx?&lt;/strong&gt; spydergsx, a former colleague I still use as a sounding board, put this better than the rest of the post does: &quot;you don&apos;t want routing baked into a config file, or your rollback becomes a config edit and reload instead of an 8-second API call.&quot; Rolling back at the edge is the same single API call the change was. Rolling back a config file is an edit, a commit, a pipeline and a reload, on every box holding a copy of it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;two-things-worth-knowing-before-you-try-it&quot;&gt;Two Things Worth Knowing Before You Try It&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Failover is not cutover.&lt;/strong&gt; In active-passive, when the primary pool recovers, traffic &lt;strong&gt;automatically returns to it&lt;/strong&gt;. I watched this happen live. East went unhealthy, west took over, east recovered, and traffic moved back on its own without me touching anything. Which means if you migrate by making the old side unhealthy, you have not migrated. You have taken a temporary detour that ends the moment somebody fixes the old health check. Reordering the pools is the only durable move.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change one side at a time.&lt;/strong&gt; The host header is used by the health monitor too, so changing it on both pools at once takes both pools unhealthy simultaneously, and failover has nowhere to go when the failure is correlated. Adaptive routing, the fallback pool, none of it saves you. Change the standby side, verify it, then cut over. Do it in that order and the safety mechanisms you are paying for can actually engage.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The shape is small enough to hold in your head. One permanent public name at the edge, an ordered list of pools underneath it, and backends that answer to the public name rather than to their own. Cutting over is a list reorder. Rolling back is the same reorder backwards. The internal hostname never reaches a user&apos;s address bar or their bookmarks.&lt;/p&gt;
&lt;p&gt;The rehearsal cost $5 and an evening, and it has already changed the real plan three times over. The new infrastructure has to answer to the final hostname &lt;em&gt;before&lt;/em&gt; anything gets pointed at it. The two sides get migrated one at a time. And the load balancer goes live with only the legacy pool in it, so the day the hostname changes hands is a day when nothing else happens. All three are obvious in hindsight and none of them were in the original plan.&lt;/p&gt;
&lt;p&gt;If you have a migration coming where a hostname needs to outlive the infrastructure under it, build the two-origin version first on a domain nobody depends on. The failure modes are identical and considerably cheaper.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[A Real Drum for a Browser Rhythm Game (ESP32 + Web Serial)]]></title><description><![CDATA[Rhythm Hands is a little browser rhythm game I built for my kid. This post wires a physical drum to it: a piezo sensor on an ESP32 DevKit, a threshold-and-debounce sketch, and Web Serial reading hit events straight into the game with no server in between. Includes the two bugs that ate the afternoon.]]></description><link>https://chrishouse.io/esp32-piezo-drum-web-serial-rhythm-game/</link><guid isPermaLink="false">https://chrishouse.io/esp32-piezo-drum-web-serial-rhythm-game/</guid><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There is a small game on this site called &lt;a href=&quot;/arcade/rhythm-hands&quot;&gt;Rhythm Hands&lt;/a&gt;. It is a learning toy for my kid: notes scroll across a staff, a yellow playhead sweeps over them, and you tap in time. Blue notes are the left hand (spacebar), red notes are the right (enter), and there are touch pads for a tablet. It teaches quarter notes and ti-ti eighths without ever showing a child a settings screen.&lt;/p&gt;
&lt;p&gt;Tapping a spacebar is fine. Hitting an actual drum is better. So this post gives the game a physical input: a piezo sensor glued to a drum surface, an ESP32 reading the knock, and the hit landing in the browser as if you&apos;d pressed the key. No app rewrite, no cloud, no server. Field report, mistakes left in, because the mistakes are the useful part.&lt;/p&gt;
&lt;figure id=&quot;rhythm-hands-demo&quot; style=&quot;width: 100%; max-width: 360px; margin: 2rem auto;&quot;&gt;
  &lt;iframe
    src=&quot;https://www.youtube-nocookie.com/embed/EYZ76-cVZTo&quot;
    title=&quot;ESP32 piezo drum demo for Rhythm Hands by Chris House&quot;
    width=&quot;360&quot;
    height=&quot;640&quot;
    style=&quot;display: block; width: 100%; height: auto; aspect-ratio: 9 / 16; border: 0; border-radius: 12px;&quot;
    loading=&quot;lazy&quot;
    referrerpolicy=&quot;strict-origin-when-cross-origin&quot;
    allow=&quot;accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
  &lt;figcaption style=&quot;margin-top: 0.75rem; text-align: center;&quot;&gt;
    A quick demo of my Rhythm Hands project. &lt;a href=&quot;https://www.youtube.com/shorts/EYZ76-cVZTo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Watch on YouTube&lt;/a&gt;.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-parts&quot;&gt;The Parts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;ESP-WROOM-32 DevKit V1&lt;/strong&gt; (the cheap dual-core board with the CP2102 USB bridge). I also had a XIAO ESP32C3 on the bench, but the C3 has no GPIO34/35, and I&apos;d already wired the sensor to 34, so the WROOM won.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;HiLetgo piezoelectric vibration sensor module&lt;/strong&gt; — a brass piezo disk on a little board, three pins: &lt;code class=&quot;language-text&quot;&gt;S&lt;/code&gt; (signal), &lt;code class=&quot;language-text&quot;&gt;+&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;-&lt;/code&gt;. Passive-ish: a knock produces a voltage spike on &lt;code class=&quot;language-text&quot;&gt;S&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A USB cable, and a laptop running Chrome.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the entire bill of materials. The interesting decisions are all in software.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-web-serial-not-wifi&quot;&gt;Why Web Serial, Not WiFi&lt;/h2&gt;
&lt;p&gt;The ESP32 can talk WiFi, so the obvious-looking design is: firmware POSTs hit events to a little local server, or opens a WebSocket to the browser. I didn&apos;t do that, and I&apos;d argue you shouldn&apos;t either for this.&lt;/p&gt;
&lt;p&gt;A drum hit is a &lt;strong&gt;latency-critical, false-trigger-sensitive&lt;/strong&gt; event. Every hop you add — router, DHCP lease, a server process to babysit, mDNS name resolution, a socket that silently drops after the laptop sleeps — is a hop that adds jitter or a 3am &quot;it just stopped working.&quot; For a thing a child whacks, &quot;plug the USB in and it works&quot; beats &quot;make sure the ESP32 joined the 2.4GHz SSID and the server is running.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Web Serial&lt;/strong&gt; (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;navigator.serial&lt;/code&gt;&lt;/a&gt;, Chrome/Edge) reads the USB serial port straight from the page. Sub-millisecond transport, no network, no server, and the debounce logic lives on the microcontroller where it belongs. The one cost is a user gesture — the browser makes you click a button and pick the port — which is a feature, not a bug, for anything touching hardware.&lt;/p&gt;
&lt;p&gt;So the contract is dead simple. The firmware prints a line per hit:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;L:1873&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and the page reads &lt;code class=&quot;language-text&quot;&gt;L&lt;/code&gt; and fires a blue hit. That&apos;s the whole protocol.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;wiring&quot;&gt;Wiring&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;HiLetgo piezo module      ESP32 DevKit V1
  S  (signal)  ---------&gt; GPIO34   (ADC1, input-only)
  -  (GND)     ---------&gt; GND
  +  (VCC)     ---------&gt; 3V3&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;GPIO34 is one of the ESP32&apos;s input-only pins (GPIO34–39). They have no internal pull resistors and no output drivers, which is exactly what you want for an analog sensor — nothing fighting the piezo. Do &lt;strong&gt;not&lt;/strong&gt; use an ADC2 pin (0, 2, 4, 12–15, 25–27) for this; ADC2 is unavailable whenever WiFi is active and behaves strangely even when it isn&apos;t.&lt;/p&gt;
&lt;p&gt;One honest note I learned the hard way below: on this module, &lt;code class=&quot;language-text&quot;&gt;+&lt;/code&gt; to 3V3 matters. Skip it and the signal pin sits dead flat.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-firmware&quot;&gt;The Firmware&lt;/h2&gt;
&lt;p&gt;The whole job is: read the analog pin, decide a knock happened, and don&apos;t let one whack count as five. A struck piezo &lt;em&gt;rings&lt;/em&gt; — one hit is a decaying burst of oscillation, not a single clean spike — so there are two pieces of debounce:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;retrigger lockout&lt;/strong&gt;: after an accepted hit, ignore the pin for 80ms so the ring counts once.&lt;/li&gt;
&lt;li&gt;A short &lt;strong&gt;peak scan&lt;/strong&gt;: once we cross threshold, follow the signal for ~12ms to report its true peak. That number is only for tuning; the game ignores it.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DrumPad&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// token the game reads: &quot;L&quot; (blue) or &quot;R&quot; (red)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;uint8_t&lt;/span&gt; pin&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;             &lt;span class=&quot;token comment&quot;&gt;// ADC-capable GPIO the sensor&apos;s signal is on&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threshold&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;// raw 12-bit value (0-4095) a hit must exceed&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; lastHitAt&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// millis() of last accepted hit (debounce)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Only the connected pad is listed so an unconnected input-only pin&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// can&apos;t pick up noise and fire phantom hits.&lt;/span&gt;
DrumPad pads&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;L&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;34&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;350&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// { &quot;R&quot;, 35, 350, 0 },   // uncomment once a red piezo is on GPIO35&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; padCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pads&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pads&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; retriggerMs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// min gap between hits on one pad&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; peakScanMs  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// how long to track the peak&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;begin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;115200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;analogReadResolution&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;// 0-4095&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; padCount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;pinMode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pads&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; INPUT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;analogSetPinAttenuation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pads&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ADC_11db&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// full ~0-3.3V range&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; now &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; padCount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    DrumPad &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;pad &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pads&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;analogRead&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// below threshold, or still inside the post-hit lockout -&gt; ignore&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threshold &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; now &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastHitAt &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; retriggerMs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// follow the spike briefly to capture its true peak&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; peak &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; started &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; started &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; peakScanMs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; sample &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;analogRead&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sample &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; peak&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; peak &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sample&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;delayMicroseconds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;peak &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threshold&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      Serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      Serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token char&quot;&gt;&apos;:&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      Serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;peak&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// e.g. &quot;L:1873\n&quot;&lt;/span&gt;
      pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastHitAt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice the second pad is commented out. I&apos;m running one sensor for now, and an unconnected input-only GPIO floats — leave GPIO35 in the scan list with nothing on it and it&apos;ll happily invent red hits from electrical noise. List only what&apos;s wired. When the second disk goes on, uncomment one line and reflash.&lt;/p&gt;
&lt;p&gt;I flashed it with the &lt;code class=&quot;language-text&quot;&gt;arduino-cli&lt;/code&gt; that ships inside Arduino IDE 2.x, so there was nothing new to install:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;CLI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/c/Users/you/AppData/Local/Programs/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe&quot;&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CLI&lt;/span&gt;&quot;&lt;/span&gt; compile &lt;span class=&quot;token parameter variable&quot;&gt;--fqbn&lt;/span&gt; esp32:esp32:esp32 rhythm-hands-esp32-drum-pads
&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CLI&lt;/span&gt;&quot;&lt;/span&gt; upload &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; COM6 &lt;span class=&quot;token parameter variable&quot;&gt;--fqbn&lt;/span&gt; esp32:esp32:esp32 rhythm-hands-esp32-drum-pads&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-receiver-in-the-browser&quot;&gt;The Receiver, in the Browser&lt;/h2&gt;
&lt;p&gt;This is the part I like: the game already speaks keypress and pointer events, so I didn&apos;t touch the game logic at all. Web Serial just calls the same &lt;code class=&quot;language-text&quot;&gt;press(&quot;L&quot;)&lt;/code&gt; the spacebar does.&lt;/p&gt;
&lt;p&gt;Opening the port is a button click, at 115200 baud to match the sketch:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; connectSerial &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; navigator &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;serial&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setSerial&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;unsupported&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Use Chrome or Edge for ESP32 USB&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; port &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;requestPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// the user gesture&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; port&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;baudRate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;115200&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  serialPortRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; port
  serialStopRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setSerial&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;connected&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ESP32 live&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;readSerial&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;port&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;readSerial&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The read loop decodes bytes and splits on newlines, buffering any partial line for next time — serial gives you a byte stream, not tidy messages, so you have to reassemble lines yourself:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; readSerial &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; decoder &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TextDecoder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;port&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;readable &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;serialStopRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reader &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; port&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;readable&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getReader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;serialStopRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; done &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; reader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;done&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
        serialBufferRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; decoder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; serialBufferRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\r?\n&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        serialBufferRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// keep the partial line&lt;/span&gt;
        lines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handleSerialLine&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      reader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;releaseLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;handleSerialLine&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the parser is deliberately forgiving. It takes the first token off the line and maps a small vocabulary to a hand, so &lt;code class=&quot;language-text&quot;&gt;L&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;L:1873&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;LEFT&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;BLUE&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt; all mean the same thing. That way I can change the firmware&apos;s mind about wording later without touching the browser:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleSerialLine &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;rawLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rawLine&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toUpperCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;line&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; line&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;[\s,:;=]+&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hand &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
    token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;L&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;LEFT&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BLUE&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;B&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;L&quot;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;R&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;RIGHT&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;RED&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;R&quot;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hand&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;press&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hand&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// exactly what the spacebar calls&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;press&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;press(&quot;L&quot;)&lt;/code&gt; is the game&apos;s existing input path — it flashes the pad, finds the nearest unplayed note within the timing window, grades it perfect/great/ok, updates the combo. The drum is now indistinguishable from the keyboard as far as the game is concerned. That&apos;s the goal: the transport is a detail, the game doesn&apos;t know or care.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;two-bugs-that-ate-the-afternoon&quot;&gt;Two Bugs That Ate The Afternoon&lt;/h2&gt;
&lt;p&gt;It did not work on the first try. It never does. Both failures were the honest kind — the ones where the tool is telling you the truth and you&apos;re misreading it.&lt;/p&gt;
&lt;h3 id=&quot;1-wrong-boot-mode-detected-0x13&quot;&gt;1. &quot;Wrong boot mode detected (0x13)&quot;&lt;/h3&gt;
&lt;p&gt;First upload:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;A fatal error occurred: Failed to connect to ESP32: Wrong boot mode
detected (0x13)! The chip needs to be in download mode.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These cheap DevKit clones often have a flaky auto-reset circuit (the little cap-and-transistor network that lets &lt;code class=&quot;language-text&quot;&gt;esptool&lt;/code&gt; drop the chip into the bootloader over DTR/RTS). When it doesn&apos;t work, you do it by hand. The trick that finally stuck: &lt;strong&gt;hold the &lt;code class=&quot;language-text&quot;&gt;BOOT&lt;/code&gt; button down through the entire &lt;code class=&quot;language-text&quot;&gt;Connecting......&lt;/code&gt; phase&lt;/strong&gt;, not the tap-EN-and-release dance. Holding IO0 low the whole time forces download mode even while &lt;code class=&quot;language-text&quot;&gt;esptool&lt;/code&gt; pulses EN. Once you see &lt;code class=&quot;language-text&quot;&gt;Writing at 0x...&lt;/code&gt; with a percentage, let go. Every reflash since has been the same little ritual.&lt;/p&gt;
&lt;h3 id=&quot;2-the-sensor-that-read-a-perfect-dead-zero&quot;&gt;2. The sensor that read a perfect, dead zero&lt;/h3&gt;
&lt;p&gt;This one was embarrassing and worth it. After flashing, I listened on the port and tapped. Nothing. Zero lines. I lowered the threshold — still nothing. I started doubting the wiring.&lt;/p&gt;
&lt;p&gt;So I flashed a throwaway diagnostic sketch that just streams the raw ADC 20 times a second, and watched the number:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; peak &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; s &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;analogRead&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;34&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; peak&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; peak &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  Serial&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;peak&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Baseline: &lt;code class=&quot;language-text&quot;&gt;0&lt;/code&gt;. Tapping: &lt;code class=&quot;language-text&quot;&gt;0&lt;/code&gt;. A &lt;em&gt;flat&lt;/em&gt; zero — not drifting noise, an actual grounded-looking zero. That is not &quot;no signal reaching the pin,&quot; that&apos;s &quot;the pin is being held at 0V.&quot; I went hunting for a short... and then realized the actual bug was two-layered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I hadn&apos;t connected &lt;code class=&quot;language-text&quot;&gt;+&lt;/code&gt; to 3V3. This particular module needs power; without it the signal output just sits at rail-bottom.&lt;/li&gt;
&lt;li&gt;And on the run where I &lt;em&gt;thought&lt;/em&gt; I was proving it dead, &lt;strong&gt;I wasn&apos;t actually tapping during the capture window.&lt;/strong&gt; A passive piezo idles at 0. A flat zero from an untouched sensor is the correct, expected reading. I had built a test that couldn&apos;t tell &quot;broken&quot; apart from &quot;you didn&apos;t hit it.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Powered it, tapped for real, and the numbers told the whole story at once:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;signal: 554
signal: 4095
signal: 733
signal: 2816
...
PEAK reading over 20s: 4095
non-zero(&gt;20) samples: 303&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Idle floor of 0, firm hits railing the ADC at 4095, light taps landing in the few-hundreds. That&apos;s a gorgeous signal — and it also handed me the threshold for free. My initial guess of 550 was needlessly deaf; with a noise floor of literally zero there&apos;s enormous headroom, so I dropped it to &lt;strong&gt;350&lt;/strong&gt; to catch lighter taps and never looked back. Reflashed the real sketch, listened again, and got clean &lt;code class=&quot;language-text&quot;&gt;L:871&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;L:1982&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;L:4095&lt;/code&gt; lines pouring out with every hit.&lt;/p&gt;
&lt;p&gt;The lesson I keep re-learning: when a sensor reads &lt;em&gt;nothing&lt;/em&gt;, build the test that shows you the raw value before you start rewiring. Half the time the sensor is fine and your test is lying to you.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-it-landed&quot;&gt;Where It Landed&lt;/h2&gt;
&lt;p&gt;Load &lt;a href=&quot;/arcade/rhythm-hands&quot;&gt;the game&lt;/a&gt; in Chrome, click &lt;strong&gt;CONNECT ESP32&lt;/strong&gt;, pick the port, and whack the drum. It fires the blue pad exactly like the spacebar — same timing window, same combo counter, same little green PERFECT.&lt;/p&gt;
&lt;p&gt;It&apos;s blue-only for the moment, which covers the early one-hand levels. The two-handed levels want a second disk on GPIO35 and one uncommented line, and because the browser&apos;s parser already understands &lt;code class=&quot;language-text&quot;&gt;R&lt;/code&gt;, that&apos;s a firmware-only change. The nicer future version drops the hands entirely — one drum that just means &quot;I hit &lt;em&gt;now&lt;/em&gt;,&quot; letting the game pick whichever note is nearest. But that&apos;s a different post. This one just needed to make a real drum play a browser game, and it does.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[A Voice Assistant on My Desk That Talks to My Local LLM (Part 5)]]></title><description><![CDATA[Part 5 of the self-hosting series: a round AMOLED ESP32-S3 on my desk that records my voice, transcribes it, asks my local Qwen, and speaks the answer back. The full pin map, the two audio chips nobody explains, and the clock-register byte that cost me a day of silence.]]></description><link>https://chrishouse.io/esp32-amoled-voice-assistant-local-llm/</link><guid isPermaLink="false">https://chrishouse.io/esp32-amoled-voice-assistant-local-llm/</guid><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;By the end of &lt;a href=&quot;/redis-agent-memory-server-ollama&quot;&gt;Part 4&lt;/a&gt;, the site had a mouth from &lt;a href=&quot;/self-hosted-llm-cloudflare-tunnel-ollama&quot;&gt;Part 3&lt;/a&gt; (a local Qwen on the desktop under my desk) and a hippocampus (Redis memory that remembers you between visits). It still lived entirely inside a browser tab. This part gives it a body: a small round screen on my actual desk that listens when I tap it, ships my voice off to be transcribed, asks the same local Qwen the website uses, and reads the answer back out loud.&lt;/p&gt;
&lt;p&gt;The hardware is a &lt;a href=&quot;https://www.waveshare.com/wiki/ESP32-S3-Touch-AMOLED-1.75&quot;&gt;Waveshare ESP32-S3-Touch-AMOLED-1.75&lt;/a&gt;. It is a lovely little board with a genuinely thin paper trail once you get past &quot;blink the screen.&quot; If you googled that board name and landed here, this is the post I wanted when I started. Fair warning, same as the rest of the series: this is a field report, written while the mistakes are still warm.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;/h2&gt;
&lt;p&gt;Three things, because that format keeps working:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A physical object on the desk that answers questions using my LLM, running on my hardware, not a cloud assistant renting me my own voice back.&lt;/li&gt;
&lt;li&gt;Have it respect the same backend flag from Part 3. When I flip the GrowthBook toggle between my house box and my friend&apos;s box, the desk gadget follows, no reflash.&lt;/li&gt;
&lt;li&gt;Do the speech-to-text and text-to-speech without standing up yet another server. The thinking stays local. The transcription and the voice can be somebody else&apos;s free compute.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;House rules from the rest of the series still hold: nothing on my network exposed without a token, and the part that reasons about my questions runs on my own GPU.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-board-honestly&quot;&gt;The Board, Honestly&lt;/h2&gt;
&lt;p&gt;Here is what is actually on it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;466x466 round AMOLED&lt;/strong&gt;, driven by a &lt;strong&gt;CO5300&lt;/strong&gt; controller over a 4-lane QSPI bus.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CST9217&lt;/strong&gt; capacitive touch panel on I2C.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;ESP32-S3&lt;/strong&gt; with 16MB flash and 8MB PSRAM, which matters, because you render a full-frame canvas in PSRAM.&lt;/li&gt;
&lt;li&gt;And the part that cost me a full day: the audio path is &lt;strong&gt;two separate chips&lt;/strong&gt;, and almost every tutorial treats it like one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is the whole reason this post has a hardware section. The display and touch are well-trodden. The audio is where people (me) lose a day.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-pin-map&quot;&gt;The Pin Map&lt;/h2&gt;
&lt;p&gt;Every pin, so you can skip the datasheet archaeology. These are the values I run in &lt;code class=&quot;language-text&quot;&gt;config.h&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Display (CO5300, QSPI)
  SCLK 38   D0 4   D1 5   D2 6   D3 7   CS 12   RST 39
  466 x 466, column offset 6

Touch (CST9217, I2C @ 0x5A)
  SDA 15   SCL 14   INT 11   RST 40

Audio I2S (shared by both codecs)
  MCLK 42   BCLK 9   WS/LRCLK 45   DOUT 8 (to speaker)   DIN 10 (from mic)
  PA enable 46  (NS4150 amplifier)

Codecs on the same I2C bus as touch (SDA 15 / SCL 14)
  ES8311  @ 0x18   output only  (DAC -&gt; NS4150 -&gt; speaker)
  ES7210  @ 0x40   input only   (dual mics -&gt; ADC -&gt; I2S DIN)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The I2C bus is shared three ways: touch, the output codec, and the input codec. The I2S bus is shared two ways: the ESP is the clock master, and both codecs are slaves hanging off the same MCLK, BCLK, and word-select lines.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-two-chips-nobody-draws&quot;&gt;The Two Chips Nobody Draws&lt;/h2&gt;
&lt;p&gt;This is the single fact that would have saved me the most time, so it gets its own heading.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ES8311&lt;/strong&gt; is a mono codec, and on this board it is wired for output. It takes I2S data on GPIO8, converts it to analog, and hands it to an &lt;strong&gt;NS4150&lt;/strong&gt; class-D amplifier that drives the speaker. The ES8311 has a microphone input on paper. On this board it is not the microphone. Do not spend an afternoon configuring its ADC. I did.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ES7210&lt;/strong&gt; is a separate 4-channel audio ADC (&lt;a href=&quot;https://www.everest-semi.com/&quot;&gt;Everest Semiconductor datasheet&lt;/a&gt;), and it is the microphone. The board&apos;s two mics feed the ES7210, which serializes them onto the I2S bus and drives GPIO10, the ESP&apos;s I2S data-in pin. It is its own I2C device at address 0x40, with its own register map, entirely independent of the ES8311.&lt;/p&gt;
&lt;p&gt;So the mental model is: &lt;strong&gt;GPIO8 goes down to the speaker chip, GPIO10 comes up from the microphone chip, and they are different silicon.&lt;/strong&gt; The ESP32 generates one set of clocks (MCLK 42, BCLK 9, WS 45) and both chips slave to them. Once that clicked, everything else was downhill.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;  Tap the orb
     |
  ES7210 mic ADC --I2S DIN(10)--&gt; ESP32-S3
     |                               |
     |                        build a 16 kHz WAV in PSRAM
     |                               |
     |                     HTTPS POST /api/assistant
     v                               v
  ES8311 &amp;lt;--I2S DOUT(8)--    Cloudflare Pages Function
  speaker                          |   Workers AI Whisper  (speech -&gt; text)
     ^                             |   local Qwen via the Part 3 tunnel  (the answer)
     |                             |   Workers AI aura-1   (text -&gt; speech, 16 kHz WAV)
     +--------- audio/wav ---------+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Whisper and aura-1 run on &lt;a href=&quot;https://developers.cloudflare.com/workers-ai/&quot;&gt;Cloudflare Workers AI&lt;/a&gt;, which has a free daily allowance and needs no box of mine. The reasoning step in the middle, the part that actually answers the question, hits the same local Qwen the website uses. The GrowthBook flag from Part 3 decides house or fever, and the device inherits that decision for free because it calls through the same function the browser does.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;doing-it-wrong-first&quot;&gt;Doing It Wrong First&lt;/h2&gt;
&lt;p&gt;My first recordings were pure silence. Not quiet audio. Digital zero, every sample. Whisper, handed four seconds of nothing, does the polite thing and hallucinates: it returned &quot;you&quot;, and once, memorably, &quot;have a great time and let me know when you are back.&quot;&lt;/p&gt;
&lt;p&gt;The debugging went in stages, each one a small archaeology dig:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I configured the ES8311&apos;s microphone. Wrong chip. See above.&lt;/li&gt;
&lt;li&gt;I found the ES7210, ported a driver, and still read zeros. The chip answered on I2C at 0x40, so it was alive. It just was not producing audio.&lt;/li&gt;
&lt;li&gt;I put the raw microphone peak into the device&apos;s telemetry, because opening the USB serial port resets the ESP32-S3 and I kept missing the boot logs. Watching the peak sit at exactly 0 over the network told me the ADC was clocked wrong, not wired wrong.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The actual bug was one byte. The ES7210 has a MAINCLK register (0x02) that sets the ADC clock divider, and for 16 kHz audio with the master clock this board runs (16000 times 256, which is 4.096 MHz), that register has to enable the clock doubler and the DLL on top of the divider. The correct value is &lt;code class=&quot;language-text&quot;&gt;0x01 | (1 &amp;lt;&amp;lt; 6) | (1 &amp;lt;&amp;lt; 7)&lt;/code&gt;, which is &lt;strong&gt;0xC1&lt;/strong&gt;. I had written &lt;code class=&quot;language-text&quot;&gt;0x01&lt;/code&gt;. With the wrong divider the ADC ran unclocked and produced flat zeros, and, being an ADC, it reported no error about it. Silence is a valid output.&lt;/p&gt;
&lt;p&gt;The fix came from the board&apos;s own repository. Waveshare ships an &lt;code class=&quot;language-text&quot;&gt;esp-idf/05_Spec_Analyzer&lt;/code&gt; example that reads the microphone for an FFT display, and it delegates to Espressif&apos;s &lt;a href=&quot;https://github.com/espressif/esp-bsp&quot;&gt;esp-bsp&lt;/a&gt; ES7210 driver. That driver has a clock coefficient table, and the row for 4.096 MHz at 16 kHz spells out every divider bit. Porting that table&apos;s values verbatim turned the peak from 0 to a live, breathing noise floor on the first flash. If you are fighting this chip, the driver is the source of truth, and the coefficient table is the part to copy exactly.&lt;/p&gt;
&lt;p&gt;One more from the same fight: do not hand-gate the clock-off register (0x01) trying to be clever about power. The esp-bsp sequence leaves it alone. So should you.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-two-core-rule&quot;&gt;The Two-Core Rule&lt;/h2&gt;
&lt;p&gt;The ESP32-S3 has two cores, and on a display device you must respect the division of labor or the screen stutters. I render the full round canvas out of PSRAM at roughly 15 frames a second on core 1. Every blocking network call, and TLS is very blocking, lives on core 0 behind a mutex-guarded snapshot. The first time I put an HTTPS poll inside the render loop, the display froze solid for the length of the request, and it was obvious within one visit that the draw loop and the radio could not share a thread.&lt;/p&gt;
&lt;p&gt;The voice recording earns its own task for a related reason. When you tap the orb, the &quot;listening&quot; cue appears instantly on core 1, but if the actual capture is queued behind a stack of network polls it can start seconds late, after you have already finished talking. Giving the recorder a dedicated task drops the tap-to-capture gap to about 60 milliseconds, so the moment the screen says listening, the microphone is genuinely open. Half my early transcription failures were not audio quality at all. They were timing, my voice landing in the silence before the window opened.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;making-it-talk-back&quot;&gt;Making It Talk Back&lt;/h2&gt;
&lt;p&gt;Getting audio out was its own small puzzle. Cloudflare&apos;s aura-1 text-to-speech returns MP3 by default, and the ESP32 has no cheap MP3 decoder I wanted to carry. The trick is that aura-1 accepts format parameters: ask for &lt;code class=&quot;language-text&quot;&gt;encoding: linear16&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;container: wav&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;sample_rate: 16000&lt;/code&gt;, and it hands back a plain PCM WAV at exactly the rate the ES8311 already plays. The device streams that WAV straight off the wire into the speaker with no decoding step.&lt;/p&gt;
&lt;p&gt;I return it from the Pages Function as a binary &lt;code class=&quot;language-text&quot;&gt;audio/wav&lt;/code&gt; body with the transcript and answer riding in response headers, rather than base64 inside JSON, which would have tripled the payload for no reason. On the device, the reply gets read into PSRAM and clocked out to the codec.&lt;/p&gt;
&lt;p&gt;The last snag was comedy: the first spoken answers sounded like a walkie-talkie. Full-scale synthesized speech was overdriving the tiny speaker into a rasp. Scaling the samples to about 60 percent gave the amplifier headroom and the voice cleaned right up. The synthesized ping and whoosh sounds had always been fine because they were generated quieter, which is exactly the clue I ignored for an hour.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;design-decisions&quot;&gt;Design Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why speech-to-text and text-to-speech live on Workers AI, not the house box.&lt;/strong&gt; The local GPU has one job worth protecting: running the model that answers questions. Whisper and aura on a free tier keep that card free for the model people actually talk to, and the transcription and the voice are commodity work. The reasoning stays home.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why the device respects the flag instead of pinning a backend.&lt;/strong&gt; Same answer as Part 3. One toggle should move everything. The desk gadget calling through the same function as the browser means it can never disagree with the website about which LLM is live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a tap and not a wake word.&lt;/strong&gt; An always-listening microphone on my desk is a thing I do not want, and a tap is a clean act of consent. The orb turns to listening only when I ask it to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why the microphone peak lives in telemetry.&lt;/strong&gt; Because opening the native USB serial port resets this board, and I kept missing the one boot log I needed. A number in the heartbeat I could watch over the network turned a guessing game into a measurement. That single move is what actually cracked the silence bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-gotchas-so-you-skip-them&quot;&gt;The Gotchas, So You Skip Them&lt;/h2&gt;
&lt;p&gt;Board-specific, hard-won, in the order they bit me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The microphone is the ES7210, a different chip from the ES8311.&lt;/strong&gt; The speaker codec cannot record on this board. Configure the ADC, not the DAC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The MAINCLK register must match your master clock.&lt;/strong&gt; For 16 kHz at 4.096 MHz MCLK, register 0x02 is 0xC1, not 0x01. Wrong divider means clean, error-free silence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy the esp-bsp ES7210 coefficient table verbatim.&lt;/strong&gt; The board&apos;s &lt;code class=&quot;language-text&quot;&gt;05_Spec_Analyzer&lt;/code&gt; example is the working reference. Do not improvise the clock bits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCLK has to be running before the ES7210 will take its configuration.&lt;/strong&gt; Bring the I2S clocks up first, then talk to the chip.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mute the amplifier (PA on GPIO46) while recording.&lt;/strong&gt; The idle class-D amp couples hiss straight into the ADC and buries your voice under a wall of noise the size of your signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Average the two microphone channels, do not sum them.&lt;/strong&gt; Summing clips on loud input, and clipped audio makes Whisper hallucinate confident nonsense.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DIN is GPIO10, DOUT is GPIO8, and they land on different chips.&lt;/strong&gt; Cross them in your head and you will chase silence on one end and confusion on the other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep all networking off the render core.&lt;/strong&gt; Any blocking call in the draw loop freezes the screen for its full duration.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Part 3 gave the site a mouth, Part 4 gave it a memory, and this part gives it a body sitting on my desk. It listens on a tap, transcribes on somebody else&apos;s free GPU, asks the same local Qwen the website asks, and speaks the answer back through a speaker the size of a coin, for a running cost of zero dollars a month plus one ESP32.&lt;/p&gt;
&lt;p&gt;The honest status: the voice loop works end to end, the reasoning is genuinely mine, and the hardware surprises were all in the audio path that the docs wave past. If you have this exact board and a weekend, the pin map and the one clock byte above are the two things that would have given me back my lost day. The rest is the fun part.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Giving the Site a Memory with Redis Agent Memory Server (Part 4)]]></title><description><![CDATA[Part 4 of the self-hosting series: wiring Redis's agent-memory-server into the site's AI chats, entirely on local hardware. Durable memory with no login, an exam coach that remembers your weak spots, and the mistakes I made getting there.]]></description><link>https://chrishouse.io/redis-agent-memory-server-ollama/</link><guid isPermaLink="false">https://chrishouse.io/redis-agent-memory-server-ollama/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;By the end of &lt;a href=&quot;/self-hosted-llm-cloudflare-tunnel-ollama&quot;&gt;Part 3&lt;/a&gt;, the site had its own LLM running on the desktop under my desk, reachable through a Cloudflare Tunnel, with a feature flag to fall back to my friend&apos;s box. One problem remained: every conversation started from zero. You could tell the chat your name, your favorite database, your deepest fears about Ingress controllers, and thirty seconds later it had the memory of a goldfish with a busy schedule.&lt;/p&gt;
&lt;p&gt;Then I found &lt;a href=&quot;https://github.com/redis/agent-memory-server&quot;&gt;agent-memory-server&lt;/a&gt;, an open source memory service from the Redis team. This post is how I wired it into the site&apos;s chats, running entirely on my own hardware, and the two or three ways I got it wrong before I got it right. Fair warning up front: I am maybe a week into using this thing. Consider this a field report from early territory, written down while the mistakes are still fresh.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;/h2&gt;
&lt;p&gt;Three things again, because that format keeps working:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Give the site&apos;s AI features durable memory. Tell the chat something once, phrase it as &quot;remember that ...&quot;, and have it recalled days later, in a different conversation, with no login and no account.&lt;/li&gt;
&lt;li&gt;Run the whole memory stack on the house box. Redis, the memory service, and every model call it makes should hit my Ollama, with no OpenAI key anywhere in the chain.&lt;/li&gt;
&lt;li&gt;The real endgame: first watch how it learns about a visitor, then flip that around and use it to help the visitor learn faster. The &lt;a href=&quot;/tools/k8s-exam-prep/&quot;&gt;k8s exam prep tool&lt;/a&gt; covers KCNA, CKA, and CKAD, and a coach that actually remembers which domains you keep bombing is a better coach than one that meets you fresh every visit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Same house rules as the rest of the series: no keys in the browser, and nothing on my network exposed without a token.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-building-blocks&quot;&gt;The Building Blocks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/redis/agent-memory-server&quot;&gt;agent-memory-server&lt;/a&gt;&lt;/strong&gt; gives an AI agent two kinds of memory. Working memory is the conversation itself, per session: you PUT the messages, it holds them with a TTL. Long-term memory is the interesting part. A background process reads the working memory, uses an LLM to extract durable facts (&quot;user prefers dark mode&quot;, &quot;user lives in Memphis&quot;), deduplicates them, embeds them, and indexes them for semantic search. You never write long-term memories by hand. You hand it conversations and it does the remembering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://redis.io/&quot;&gt;Redis&lt;/a&gt;&lt;/strong&gt; is the storage under all of it: the sessions, the extracted memories, and the vector index the semantic search runs on. I am running the redis-stack image in Docker next to the memory server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;&lt;/strong&gt;, still the workhorse from Part 3. The memory server needs two models of its own: an embedding model for the vector search and a generation model for the fact extraction. Both point at the same Ollama that serves the chat, which means the entire memory system runs without a single external API call.&lt;/p&gt;
&lt;p&gt;The whole stack is one Docker Compose file:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;redis-memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; redis/redis&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;agent-memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; redislabs/agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;memory&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;REDIS_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//redis&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;memory&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6379&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;OLLAMA_API_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//host.docker.internal&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11434&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;GENERATION_MODEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ollama/qwen2.5&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3b
      &lt;span class=&quot;token key atrule&quot;&gt;EMBEDDING_MODEL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ollama/nomic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;embed&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;text&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Browser (Neon GPT, Station Intelligence, exam coach)
  │  POST /api/llm or /api/ask   { messages, memory: { userId, sessionId } }
  ▼
Cloudflare Pages Function
  │  recall:  POST /v1/memory/prompt      (before generating)
  │  store:   PUT  /v1/working-memory/…   (after answering, in the background)
  ▼
memory.chrishouse.io   (Cloudflare Tunnel + Access service token)
  ▼
agent-memory-server ──► Redis (sessions, facts, vectors)
        │
        └──► Ollama (extraction + embeddings, same box as the chat model)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The tunnel and Access setup is a straight copy of Part 3: a second hostname on the same tunnel, path-restricted to the API, behind its own service token. Anyone hitting &lt;code class=&quot;language-text&quot;&gt;memory.chrishouse.io&lt;/code&gt; without the token gets a 403 from Cloudflare&apos;s edge before the request ever reaches my house.&lt;/p&gt;
&lt;p&gt;Identity is an anonymous id the browser mints once and keeps in localStorage. No accounts, no emails, no names unless you volunteer one to the chat. The memory is scoped to that id, so your remembered facts are yours and the next visitor gets their own clean slate.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;doing-it-wrong-first&quot;&gt;Doing It Wrong First&lt;/h2&gt;
&lt;p&gt;My first integration treated the memory server like a database. Every chat turn, my code wrote its own long-term records: one episodic record of the exchange, plus a &quot;durable fact&quot; record whenever someone said &quot;remember that ...&quot;. For recall, it ran four separate searches (semantic, keyword, hybrid, and a topic-filtered one) and stitched the results into the prompt by hand.&lt;/p&gt;
&lt;p&gt;It worked, in the sense that a car with square wheels technically moves. What I actually built was a duplicate factory. The server was extracting memories from the conversations on its own, like it is designed to, while my code wrote overlapping records next to them. Within a day of testing, the store contained &quot;chris likes pineapple pizza&quot; four times, a server-merged memory that said pepperoni instead, and an episodic transcript of me asking about pizza. The site held contradictory pizza beliefs simultaneously, which is very human of it, and also useless.&lt;/p&gt;
&lt;p&gt;The fix was deleting code. The idiomatic pattern is embarrassingly small, so here it is as pseudo code instead of a wall of JavaScript:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;store(turn):
    PUT working-memory(sessionId, messages)
    # that&apos;s it. the server extracts, dedups, and indexes in the background

recall(question):
    POST memory/prompt(query, sessionId, semantic search over this user)
    # returns the session context plus relevant long-term memories,
    # already formatted to drop into the system prompt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One write, one read. Half the GPU work, no duplicate records, and the server&apos;s own deduplication actually gets a chance to do its job. If you take one thing from this post: do not hand-write long-term memories. The extraction pipeline is the product.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-extraction-model-matters-more-than-i-expected&quot;&gt;The Extraction Model Matters More Than I Expected&lt;/h2&gt;
&lt;p&gt;The memory server&apos;s extraction step is an LLM call, and my first configuration pointed it at the model I already had loaded: qwen2.5-coder, the code-tuned model that answers the chat. It extracted exactly zero memories. Not bad memories. Zero. A code model asked to distill &quot;remember that my keyboard is a HHKB and I live in Memphis&quot; into facts just stares at you and thinks about brackets.&lt;/p&gt;
&lt;p&gt;Swapping to a general instruct model fixed extraction immediately, and then created a new problem: the 7B version plus the 14B chat model plus the embedding model added up to more VRAM than the card has, and Ollama started playing musical chairs with 16GB of memory. At one point the chat model got stuck mid-eviction and the whole endpoint hung. The 3B instruct model extracts nearly as well, fits alongside everything else, and the musical chairs stopped.&lt;/p&gt;
&lt;p&gt;The lesson generalizes: extraction quality is a function of the model doing the extracting, and it is a separate decision from your chat model. Small general instruct beats large code-tuned at this job by an infinite margin, because anything beats zero.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-it-powers-today&quot;&gt;What It Powers Today&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&quot;Remember that ...&quot; in the chats.&lt;/strong&gt; Tell &lt;a href=&quot;/tools/chat&quot;&gt;Neon GPT&lt;/a&gt; or &lt;a href=&quot;/mission-control/&quot;&gt;Station Intelligence&lt;/a&gt; to remember something, come back tomorrow in a fresh conversation, and ask about it. The recall goes through the same memory prompt as everything else, so facts surface even when you ask sideways (&quot;what keyboard do I use?&quot; finds a memory that never contained those exact words).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The station recognizes regulars.&lt;/strong&gt; When Station Intelligence boots and memory exists for your browser id, the greeting is composed from what it knows. Mine mentions blue fish, for reasons I have fully earned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The exam coach.&lt;/strong&gt; This is the part I care about. Finish a quiz or practice exam on &lt;a href=&quot;/tools/k8s-exam-prep/&quot;&gt;k8s exam prep&lt;/a&gt; and the page logs a summary to memory: score, domains, where you were weak. The coach button recalls that history and prescribes what to drill today. Next to it, a &quot;drill weak spots&quot; button builds your next quiz directly from the questions you have missed and your lowest-mastery domains, and those results feed back into memory. The loop closes: study, get remembered, get coached, drill the gap, repeat.&lt;/p&gt;
&lt;p&gt;That third one is the experiment I wanted to run all along. The site learning my pizza order is a party trick. The site remembering that I cannot tell a NetworkPolicy from a hole in the ground, and quietly building tomorrow&apos;s study session around that fact, is the thing that might actually be worth the Redis container.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;early-days-honestly&quot;&gt;Early Days, Honestly&lt;/h2&gt;
&lt;p&gt;A week in, here is what is rough:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Contradictions accumulate.&lt;/strong&gt; Telling the chat &quot;I no longer like pineapple pizza&quot; adds a new memory next to the old one. The server has forget and compact endpoints built for exactly this, and I have not wired them up yet. Right now the model gets both facts and has to sort out the timeline itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One enrichment step fails on my Redis.&lt;/strong&gt; The topic/entity tagging task uses a command that only exists in Redis 8, and the redis-stack image I deployed predates it. Core memory works fine (store, search, recall are all healthy), but every extraction logs an error for the metadata step. The fix is moving to the redis:8 image, which now has vector search built in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extraction is eventually consistent.&lt;/strong&gt; Facts land seconds after the conversation, on a debounce, after a background model call. Say &quot;remember X&quot; and immediately ask about X in a new session and you can beat the pipeline. In practice nobody chats that way, but tests do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My own bug was the best one.&lt;/strong&gt; After the refactor, every &quot;remember that ...&quot; reported a failed write, because my handler still checked a response field the old code returned and the new code did not. The writes were succeeding the whole time. The site was gaslighting itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these have made me regret the setup. All of them are the kind of thing you only learn by running it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;design-decisions&quot;&gt;Design Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why memory lives behind the Pages Function.&lt;/strong&gt; The browser never talks to the memory server. The function resolves the identity, loads memory into the prompt, and stores the turn afterward in the background, so a slow memory call never delays the answer. If the memory server is down, everything fails open and the chat just answers without memory. The site loses its memory gracefully, like the rest of us.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why the identity is anonymous.&lt;/strong&gt; An id in localStorage costs nothing, needs no consent screen beyond what analytics already covers, and gives exactly the scoping memory needs. Accounts would make the memory better bound and the site worse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why extraction runs on a small model.&lt;/strong&gt; Covered above, but as a decision: the extraction model is infrastructure, and infrastructure should be the smallest thing that does the job, especially when it shares a GPU with the model people are actually talking to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am writing this now instead of after a month.&lt;/strong&gt; Because the early mistakes are the useful content. A month from now the duplicate factory and the code-model-extracts-nothing discovery would be smoothed over into &quot;configure it correctly&quot;, and you would step on both rakes yourself.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-gotchas-so-you-skip-them&quot;&gt;The Gotchas, So You Skip Them&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Do not write long-term memories by hand.&lt;/strong&gt; PUT working memory and let the server extract. Hand-written records fight the deduplication and double your GPU load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a general instruct model for extraction.&lt;/strong&gt; Code models extract nothing. Check your VRAM budget: chat model + extraction model + embeddings all resident at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session and user ids have a minimum length.&lt;/strong&gt; Short test ids get silently replaced by fallbacks, and then your writes and reads disagree about identity. An hour of my life, gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the response shape after upgrades.&lt;/strong&gt; The memory prompt endpoint returns message content as objects, and my code assumed strings. Fail-open code hides that kind of bug until you go looking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Old Redis, new commands.&lt;/strong&gt; If extraction logs unknown-command errors, your Redis predates what the memory server expects. Core features may still work, which makes it easy to miss.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The stack from Parts 1 through 3 gave the site a mouth. This part gives it a hippocampus, one Docker Compose file and a tunnel hostname away, running on the same GPU as everything else for the same monthly cost of zero dollars.&lt;/p&gt;
&lt;p&gt;I am early on this, and the honest status is: durable memory works, recall is better than I expected, hygiene needs work, and the contradiction problem is real but survivable. The experiment that matters is the exam coach. If a site with memory can watch you study, notice what you get wrong, and shape what you practice next, that is a genuinely different thing from a chat widget with a good personality. Ask me in a month whether it made me faster at the CKA. The site will remember either way.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Running Your Own LLM Endpoint with Cloudflare Tunnels and Ollama (Part 3)]]></title><description><![CDATA[Part 3 of the local-LLM saga: when a friend's GPU goes offline, I stood up my own Qwen on Ollama, exposed it over a Cloudflare Tunnel locked behind Access, and wired a GrowthBook flag so the blog can flip between his box and mine from the chat header.]]></description><link>https://chrishouse.io/self-hosted-llm-cloudflare-tunnel-ollama/</link><guid isPermaLink="false">https://chrishouse.io/self-hosted-llm-cloudflare-tunnel-ollama/</guid><pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the third stop in a series that started with a friend&apos;s GPU. &lt;a href=&quot;/opencode-part-1&quot;&gt;Part 1&lt;/a&gt; pointed OpenCode at his local Qwen. &lt;a href=&quot;/cloudflare-ai-gateway-turnstile&quot;&gt;Part 2&lt;/a&gt; wired this blog&apos;s AI features to that same box through a Cloudflare AI Gateway, with Turnstile keeping bots off his GPU. It all worked great, right up until his machine went dark for a weekend and every AI feature on the site started returning shrugs.&lt;/p&gt;
&lt;p&gt;His box can go down whenever it wants, and I do not get a vote. So I wanted my own copy of the model running on my own hardware, ready to take over when his machine naps. This post is how I did that with &lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt; and a &lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&quot;&gt;Cloudflare Tunnel&lt;/a&gt;, locked down so I am the only one who gets to spend my electricity.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;/h2&gt;
&lt;p&gt;Three things, in plain terms:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Serve a Qwen model from the desktop under my desk, the one with the RTX 4080 Super in it.&lt;/li&gt;
&lt;li&gt;Reach it from &lt;code class=&quot;language-text&quot;&gt;blog.chrishouse.io&lt;/code&gt;, which runs on Cloudflare&apos;s edge and has no idea what my home network looks like. No port forwarding, no static IP, no exposing my home address to the internet.&lt;/li&gt;
&lt;li&gt;Flip between my box and my friend&apos;s box on demand, without a redeploy, and see instantly whether the one I picked is actually alive.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The whole thing had to keep the existing rule from Part 1: no keys in the browser, and nobody but my own site gets to use the GPU.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-building-blocks&quot;&gt;The Building Blocks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;&lt;/strong&gt; runs local models and hands you an OpenAI-compatible endpoint at &lt;code class=&quot;language-text&quot;&gt;http://localhost:11434/v1&lt;/code&gt;. That compatibility is the whole trick. From the blog&apos;s point of view a box under my desk looks identical to any other &lt;code class=&quot;language-text&quot;&gt;POST /v1/chat/completions&lt;/code&gt; provider, which is exactly what Part 1&apos;s proxy already speaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&quot;&gt;Cloudflare Tunnel&lt;/a&gt;&lt;/strong&gt; (the &lt;code class=&quot;language-text&quot;&gt;cloudflared&lt;/code&gt; daemon) makes an outbound-only connection from my PC to Cloudflare&apos;s edge and publishes a local port at a public hostname. My home IP stays hidden, my router stays closed, and &lt;code class=&quot;language-text&quot;&gt;llm.chrishouse.io&lt;/code&gt; resolves to a machine that never accepted an inbound connection in its life.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/policies/access/&quot;&gt;Cloudflare Access&lt;/a&gt;&lt;/strong&gt; sits in front of that hostname and rejects anyone without a valid service token. A public tunnel to an unauthenticated Ollama is a free GPU for the entire internet, so this part is not optional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.growthbook.io/&quot;&gt;GrowthBook&lt;/a&gt;&lt;/strong&gt; holds a single feature flag, &lt;code class=&quot;language-text&quot;&gt;llm-backend&lt;/code&gt;, whose value is &lt;code class=&quot;language-text&quot;&gt;house&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;fever&lt;/code&gt;. The blog reads it to decide which upstream to call. Same tool that already runs the flag demos on this site, now doing something I actually depend on.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Browser (Neon GPT)
  │  POST /api/llm   { messages, backend }
  ▼
Cloudflare Pages Function  (origin gate → session check → sanitize → pick backend)
  │
  ├── backend = &quot;fever&quot; ─► Cloudflare AI Gateway ─► LiteLLM ─► friend&apos;s Qwen   (Part 1)
  │
  └── backend = &quot;house&quot; ─► https://llm.chrishouse.io
                              │  CF-Access-Client-Id / CF-Access-Client-Secret
                              ▼
                          Cloudflare Access   (403 without the token)
                              │
                              ▼
                          cloudflared tunnel  (Windows service)
                              │  http host header rewritten to localhost:11434
                              ▼
                          Ollama ─► Qwen (my RTX 4080 Super)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The flag decides which branch runs. The proxy holds both sets of credentials server-side and picks one per request. The browser never learns either endpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;standing-up-ollama&quot;&gt;Standing Up Ollama&lt;/h2&gt;
&lt;p&gt;Installing Ollama and pulling a model is two commands, and I already had a few models on disk. The part worth writing down is which model to serve, because I got this wrong first.&lt;/p&gt;
&lt;p&gt;Qwen3 is a reasoning model. Left alone it burns a few hundred tokens thinking before it says hello. Part 1 disabled that with &lt;code class=&quot;language-text&quot;&gt;chat_template_kwargs: { enable_thinking: false }&lt;/code&gt;, and my friend&apos;s LiteLLM honored it. Ollama&apos;s OpenAI-compatible endpoint does not. I tried every switch (&lt;code class=&quot;language-text&quot;&gt;think:false&lt;/code&gt;, the &lt;code class=&quot;language-text&quot;&gt;/no_think&lt;/code&gt; prompt token, the &lt;code class=&quot;language-text&quot;&gt;enable_thinking&lt;/code&gt; kwarg) and Ollama&apos;s &lt;code class=&quot;language-text&quot;&gt;/v1&lt;/code&gt; route ignored all of them. The answer kept landing in a &lt;code class=&quot;language-text&quot;&gt;reasoning&lt;/code&gt; field with &lt;code class=&quot;language-text&quot;&gt;content&lt;/code&gt; sitting there empty, which the blog then rendered as a blank message.&lt;/p&gt;
&lt;p&gt;So I served a model that does not think out loud:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ollama pull qwen2.5-coder:14b&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At a Q4 quant it is about 9 GB, which sits entirely in the 4080 Super&apos;s 16 GB of VRAM with room to spare, and it answers in well under a second. Coder-tuned, but for a general chat widget it holds a conversation fine. A quick sanity check straight against Ollama:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; http://localhost:11434/v1/chat/completions &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{&quot;model&quot;:&quot;qwen2.5-coder:14b&quot;,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;say hi&quot;}],&quot;stream&quot;:false}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Real &lt;code class=&quot;language-text&quot;&gt;content&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;finish_reason: stop&lt;/code&gt;, no empty replies. Good enough to put behind a tunnel.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-tunnel&quot;&gt;The Tunnel&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;cloudflared&lt;/code&gt; needs a login (a browser SSO handshake against your Cloudflare account), then a named tunnel and a DNS route:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;cloudflared tunnel login
cloudflared tunnel create theblog-llm
cloudflared tunnel route dns theblog-llm llm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chrishouse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;io&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then a config file. This is where the second gotcha lives. Recent Ollama refuses any request whose &lt;code class=&quot;language-text&quot;&gt;Host&lt;/code&gt; header is not localhost, a sensible anti-drive-by measure. Through the tunnel Ollama sees &lt;code class=&quot;language-text&quot;&gt;Host: llm.chrishouse.io&lt;/code&gt; and answers with a fast, confusing 403. The fix is one line that rewrites the host header back to what Ollama expects:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;tunnel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; theblog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;llm
&lt;span class=&quot;token key atrule&quot;&gt;credentials-file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; C&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;\Users\Chris\.cloudflared\&amp;lt;tunnel&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;uuid&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;.json
&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; llm.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11434&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;originRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;httpHostHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11434&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http_status&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;cloudflared tunnel run theblog-llm&lt;/code&gt; and the hostname goes live. To survive reboots it runs as a Windows service. One warning there: a bare &lt;code class=&quot;language-text&quot;&gt;cloudflared service install&lt;/code&gt; registers the service with no run arguments, so it starts, prints its help text, and exits half a second later. I set an explicit command on the service so it runs my tunnel with my config:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;C:\Users\Chris\tools\cloudflared.exe&quot; --config &quot;C:\Users\Chris\.cloudflared\config.yml&quot; tunnel run theblog-llm&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Automatic startup, comes back after a reboot, done.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;locking-it-down-with-access&quot;&gt;Locking It Down with Access&lt;/h2&gt;
&lt;p&gt;At this point &lt;code class=&quot;language-text&quot;&gt;llm.chrishouse.io&lt;/code&gt; is a public URL pointed at an LLM with no authentication. Anyone who finds it gets free inference on my GPU. Cloudflare Access closes that with a service token, which is a client ID and secret built for machine-to-machine calls.&lt;/p&gt;
&lt;p&gt;I created the service token, a self-hosted Access application on the hostname, and a policy that allows only that token. The blog&apos;s proxy presents the token on every call to the house backend:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; headers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;key&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_ACCESS_CLIENT_ID_HOUSE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_ACCESS_CLIENT_SECRET_HOUSE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  headers&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CF-Access-Client-Id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_ACCESS_CLIENT_ID_HOUSE&lt;/span&gt;
  headers&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CF-Access-Client-Secret&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_ACCESS_CLIENT_SECRET_HOUSE&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The proof is two curls. Without the token, Access stops the request at the edge before it ever reaches my house:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;$ curl -s -o /dev/null -w &apos;%{http_code}\n&apos; https://llm.chrishouse.io/v1/chat/completions -d &apos;{}&apos;
403

$ curl -s https://llm.chrishouse.io/v1/chat/completions \
    -H &quot;CF-Access-Client-Id: &amp;lt;id&gt;&quot; -H &quot;CF-Access-Client-Secret: &amp;lt;secret&gt;&quot; \
    -d &apos;{&quot;model&quot;:&quot;qwen2.5-coder:14b&quot;,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;reply OK&quot;}],&quot;stream&quot;:false}&apos;
{&quot;choices&quot;:[{&quot;message&quot;:{&quot;content&quot;:&quot;OK&quot;}}], ...}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;403 for the world, 200 for the blog. That is the whole security model in two lines of output.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;growthbook-is-the-toggle&quot;&gt;GrowthBook Is the Toggle&lt;/h2&gt;
&lt;p&gt;The switch itself is a single GrowthBook flag named &lt;code class=&quot;language-text&quot;&gt;llm-backend&lt;/code&gt;, and its value is either &lt;code class=&quot;language-text&quot;&gt;house&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;fever&lt;/code&gt;. That flag is the whole point. Nothing in the browser decides which model you talk to, and I never touch an environment variable to change it. I flip the flag and the entire site follows.&lt;/p&gt;
&lt;p&gt;A small endpoint reads the flag and writes it through the GrowthBook Admin API, so the choice is shared state instead of a setting stuck in one person&apos;s browser tab. Change it once and everyone hitting the site moves to the same backend at the same time. When my friend&apos;s box comes back online, one flip sends everyone back to it.&lt;/p&gt;
&lt;p&gt;Behind the flag, the proxy still has to turn the word &lt;code class=&quot;language-text&quot;&gt;house&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;fever&lt;/code&gt; into a real endpoint with the right credentials. I kept the naming boring: the base environment variables are the &lt;code class=&quot;language-text&quot;&gt;fever&lt;/code&gt; backend from Part 1, and a parallel set with a &lt;code class=&quot;language-text&quot;&gt;_HOUSE&lt;/code&gt; suffix is my box. A tiny resolver maps the flag value to one of them:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;resolve(flag):
    if flag is &quot;house&quot;:            # my box
        endpoint = the *_HOUSE vars (local Qwen via the tunnel)
        auth     = Cloudflare Access service token
    else:                          # fever, the Part 1 path
        endpoint = the base vars (feverdreams gateway)
        auth     = Cloudflare AI Gateway token

    return endpoint, model, auth headers&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Same two lines of config, two different locks. Fever carries the AI Gateway token from Part 1, house carries the Access service token, and the flag is the only thing that decides which one runs. Adding my box was a config change, and the Part 1 path never moved.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-you-can-test-it&quot;&gt;Where You Can Test It&lt;/h2&gt;
&lt;p&gt;Go poke at &lt;a href=&quot;/tools/chat&quot;&gt;Neon GPT&lt;/a&gt;. Up in the header there is a &lt;strong&gt;House / Fever&lt;/strong&gt; toggle sitting next to the connection dot.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;House&lt;/strong&gt; routes your message to the Qwen on my desk, through the tunnel, through Access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fever&lt;/strong&gt; routes it to my friend&apos;s box, the Part 1 path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flip the toggle and two things happen. It writes the GrowthBook flag, then it fires a tiny health-check ping at the backend you picked and repaints the dot: green if that model answered, red if it did not. So if you switch to House and the dot goes red, my desktop is off, or Ollama crashed, or I am playing a game and told it to stop hogging the GPU. The chat degrades to a clear &quot;that model is offline&quot; message instead of hanging.&lt;/p&gt;
&lt;p&gt;It is a real switch on a real thing. Be nice to my electric bill.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;design-decisions&quot;&gt;Design Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why a tunnel instead of port forwarding?&lt;/strong&gt; Port forwarding means opening my router to the internet and handing out my home IP, then hoping nothing else on my network has a bad day. The tunnel is outbound-only, hides the IP, and gives me a real hostname with a real certificate for free. Less attack surface and less DNS busywork.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a service token instead of a login page?&lt;/strong&gt; The only caller is a server, my Pages Function. Human login flows are the wrong shape for that. A service token is two secret headers, it lives in the Pages environment next to the other keys, and it never involves a browser. Access checks it at the edge, so a bad request dies in Cloudflare&apos;s datacenter and never touches my house.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a feature flag instead of just editing an env var?&lt;/strong&gt; Because an env var edit is a redeploy, and redeploys are slow when a model is down and the chat is broken right now. The flag moves everyone in one API call, and I can flip it from my phone. It also means the fallback is a product decision I can make live, not a code path I have to ship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a non-reasoning model?&lt;/strong&gt; Because Ollama&apos;s &lt;code class=&quot;language-text&quot;&gt;/v1&lt;/code&gt; endpoint would not let me turn Qwen3&apos;s thinking off, and a reasoning model that spends its whole token budget thinking returns an empty chat bubble. A coder-tuned 14B that fits fully in VRAM and answers immediately is the better trade for a chat toy, and it sidesteps the whole fight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why keep my friend&apos;s box as the default?&lt;/strong&gt; His is a beefier 27B and it is usually up. Mine is the safety net for when it is not. Default to the better model, fall back to the reliable one, flip in a second when reality disagrees.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-gotchas-so-you-skip-them&quot;&gt;The Gotchas, So You Skip Them&lt;/h2&gt;
&lt;p&gt;A short list of things that ate my afternoon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ollama 403 through the tunnel.&lt;/strong&gt; Host header. Rewrite it to &lt;code class=&quot;language-text&quot;&gt;localhost:11434&lt;/code&gt; in the ingress config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty chat replies.&lt;/strong&gt; Reasoning model. Ollama&apos;s &lt;code class=&quot;language-text&quot;&gt;/v1&lt;/code&gt; ignores every thinking switch, so serve a non-reasoning model or read the &lt;code class=&quot;language-text&quot;&gt;reasoning&lt;/code&gt; field as a fallback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service starts then dies.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;cloudflared service install&lt;/code&gt; leaves no run arguments. Set the service command explicitly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A zombie worker.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;wrangler pages dev&lt;/code&gt; runs the real worker as a separate &lt;code class=&quot;language-text&quot;&gt;workerd&lt;/code&gt; process. A dev server I killed days ago left one holding the port, so every &quot;fresh&quot; restart was quietly serving stale environment variables and swearing my new config did not exist. Kill the orphaned &lt;code class=&quot;language-text&quot;&gt;workerd&lt;/code&gt;, confirm the port is actually free, then start over. I lost an hour to this one and I want that hour back.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The shape from Part 1 held up. An OpenAI-compatible upstream, a thin Pages Function for secrets and sanitizing, and now a second upstream that happens to be a computer in my house. Ollama makes the model boring, the tunnel makes it reachable without opening my network, Access makes it mine alone, and one GrowthBook flag turns &quot;which model&quot; into a switch instead of a deploy.&lt;/p&gt;
&lt;p&gt;If you have a decent GPU sitting idle and a public site that wants an LLM, this is a weekend of work and zero dollars a month. Serve the model, tunnel it out, lock it behind a token, and give yourself a flag so the day your dependency goes dark, you are one toggle away from your own hardware picking up the slack.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Cloudflare AI Gateway and Turnstile: Fronting a Friend's Local Qwen Endpoint (Part 2)]]></title><description><![CDATA[How this blog's AI features route through a Cloudflare AI Gateway to a friend's locally hosted Qwen model behind LiteLLM, with Turnstile and HMAC session tokens keeping bots off the GPU.]]></description><link>https://chrishouse.io/cloudflare-ai-gateway-turnstile/</link><guid isPermaLink="false">https://chrishouse.io/cloudflare-ai-gateway-turnstile/</guid><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This blog has a handful of LLM-powered features: &lt;a href=&quot;/tools/chat&quot;&gt;Neon GPT&lt;/a&gt; (a no-login chat), the world generator in &lt;a href=&quot;/blocks&quot;&gt;Blocks&lt;/a&gt;, and the live slide remixer in the arcade&apos;s PowerPoint Karaoke game. None of them call a commercial API. The model is a Qwen instance running on a friend&apos;s hardware, served through LiteLLM, and every request from this site reaches it through a Cloudflare AI Gateway that I own.&lt;/p&gt;
&lt;p&gt;That arrangement creates a specific problem: the endpoint burns my friend&apos;s electricity and GPU time, and the frontend is a public static site with no accounts. Anyone can open dev tools, find the API call, and script it. The answer ended up being three layers: an AI Gateway for visibility and control, a thin Cloudflare Pages Function so no keys ever ship to the browser, and Turnstile to make sure a real browser on my site is asking.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-building-blocks&quot;&gt;The Building Blocks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/ai-gateway/&quot;&gt;Cloudflare AI Gateway&lt;/a&gt;&lt;/strong&gt; is a proxy that sits between your application and an LLM provider. You create a gateway in the Cloudflare dashboard, point your app at the gateway URL instead of the provider, and every request flows through Cloudflare on its way upstream. In exchange you get analytics (request counts, tokens, latency, errors), full request/response logging, response caching, rate limiting, and retry/fallback behavior, all configured per-gateway without touching application code. It supports the big commercial providers, but it will also front any OpenAI-compatible endpoint, which is what makes it useful here: the &quot;provider&quot; behind my gateway is a friend&apos;s machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developers.cloudflare.com/turnstile/&quot;&gt;Cloudflare Turnstile&lt;/a&gt;&lt;/strong&gt; is Cloudflare&apos;s CAPTCHA replacement. The widget runs in the browser (there&apos;s an invisible mode with no puzzle to solve), produces a short-lived token, and your server verifies that token against the &lt;code class=&quot;language-text&quot;&gt;siteverify&lt;/code&gt; API. A passing token means a real browser executed the challenge on a page with your sitekey. It&apos;s free, and it&apos;s the entire reason &lt;code class=&quot;language-text&quot;&gt;curl&lt;/code&gt; can&apos;t talk to my LLM proxy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/BerriAI/litellm&quot;&gt;LiteLLM&lt;/a&gt;&lt;/strong&gt; is an open-source proxy that exposes 100+ model backends behind one OpenAI-compatible API. My friend runs it in front of a locally hosted &lt;a href=&quot;https://github.com/QwenLM/Qwen3&quot;&gt;Qwen3&lt;/a&gt; model (a 27B running an NVFP4 quant). From my side of the wire, it looks exactly like the OpenAI chat completions API: &lt;code class=&quot;language-text&quot;&gt;POST /v1/chat/completions&lt;/code&gt;, same request shape, same response shape. That compatibility is what lets the AI Gateway and my Pages Function treat a hobby GPU box like any other provider.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Browser (Neon GPT, Blocks, arcade games)
  │  POST /api/llm         + X-LLM-Session header
  ▼
Cloudflare Pages Function  (origin allowlist → rate limit → session check → sanitize)
  │  Authorization: Bearer &amp;lt;gateway key&gt;
  │  cf-aig-authorization: Bearer &amp;lt;AI Gateway token&gt;
  ▼
Cloudflare AI Gateway      (logging, analytics, caching, kill switch)
  │
  ▼
LiteLLM (friend&apos;s box)     (OpenAI-compatible proxy)
  │
  ▼
Qwen3 27B (local GPU)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the session side-channel that feeds the &lt;code class=&quot;language-text&quot;&gt;X-LLM-Session&lt;/code&gt; header:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Browser ── invisible Turnstile widget ──&gt; turnstile token
  │  POST /api/llm-session { turnstileToken }
  ▼
Pages Function ── siteverify ──&gt; Cloudflare
  │  (valid → mint HMAC session token, 10 min TTL)
  ▼
Browser caches token, attaches it to every /api/llm call&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Nothing secret ever reaches the browser. The gateway URL, the upstream key, the AI Gateway token, the Turnstile secret, and the session-signing secret all live as encrypted Cloudflare environment variables.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;setting-up-the-ai-gateway&quot;&gt;Setting Up the AI Gateway&lt;/h2&gt;
&lt;p&gt;In the Cloudflare dashboard: &lt;strong&gt;AI → AI Gateway → Create Gateway&lt;/strong&gt;. That gives you a gateway endpoint. Because the upstream here is a custom OpenAI-compatible server rather than a named provider, requests address it in passthrough style. The gateway forwards to the configured upstream and records everything that goes through.&lt;/p&gt;
&lt;p&gt;Two settings matter for this build:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Authenticated Gateway.&lt;/strong&gt; By default anyone who discovers your gateway URL can send requests through it (and to your upstream). Turning on authentication makes the gateway reject any request that doesn&apos;t carry a &lt;code class=&quot;language-text&quot;&gt;cf-aig-authorization&lt;/code&gt; header with a token you generate in the dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs.&lt;/strong&gt; Request/response logging is per-gateway. For a setup where someone else pays the GPU bill, the log view is the accountability layer. I can see exactly what traffic I sent him, token counts included.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The server-side call that goes through the gateway lives in a Pages Function (&lt;code class=&quot;language-text&quot;&gt;functions/api/llm.js&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; base &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_GATEWAY_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\/+$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; upstreamHeaders &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_GATEWAY_KEY&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// When routing through a Cloudflare AI Gateway (authenticated), add its gateway token.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_AIG_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; upstreamHeaders&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cf-aig-authorization&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_AIG_TOKEN&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

upstream &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;base&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/chat/completions&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; upstreamHeaders&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two credentials, two different locks: &lt;code class=&quot;language-text&quot;&gt;Authorization&lt;/code&gt; is the LiteLLM key the upstream expects, &lt;code class=&quot;language-text&quot;&gt;cf-aig-authorization&lt;/code&gt; is the AI Gateway&apos;s own token. The &lt;code class=&quot;language-text&quot;&gt;LLM_AIG_TOKEN&lt;/code&gt; check means the same code runs with or without a gateway in the path. Unset it and the function calls the provider directly.&lt;/p&gt;
&lt;p&gt;One Qwen-specific detail in the payload: Qwen3 is a reasoning model, and by default it will happily spend hundreds of tokens thinking before it answers. For interactive features that&apos;s latency you can feel, so thinking is off unless a caller explicitly asks for it:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; payload &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_MODEL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;model &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;qwen-3.6-27b-nvfp4&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; safeMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;temperature&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;numOr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;temperature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;numOr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;max_tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2048&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Qwen3 is a reasoning model, so disable thinking for fast, direct answers&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;chat_template_kwargs&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;enable_thinking&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;think &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-pages-function-proxy&quot;&gt;The Pages Function Proxy&lt;/h2&gt;
&lt;p&gt;Cloudflare Pages automatically deploys anything in &lt;code class=&quot;language-text&quot;&gt;/functions&lt;/code&gt; as a serverless function, so &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt; is one file with no infrastructure. It does four jobs before anything reaches the gateway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Origin gating.&lt;/strong&gt; Only requests originating from this site (or localhost during dev) are allowed:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;origin &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;allowed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;origin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Forbidden&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; Cloudflare&apos;s &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/&quot;&gt;rate limiting binding&lt;/a&gt; is wired per-IP, written so it no-ops until the binding is configured:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RATE_LIMITER&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RATE_LIMITER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;limit &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;function&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ip &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CF-Connecting-IP&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; origin &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anon&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RATE_LIMITER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ip &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Rate limited, slow down.&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;429&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Input capping.&lt;/strong&gt; Whatever the client sends gets clamped before it can cost anything: last 16 messages only, 6,000 characters per message, &lt;code class=&quot;language-text&quot;&gt;max_tokens&lt;/code&gt; capped at 2048, roles whitelisted:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; safeMessages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Response normalization.&lt;/strong&gt; Every caller on the site gets the same tiny shape back, &lt;code class=&quot;language-text&quot;&gt;{ content, usage }&lt;/code&gt;, regardless of what the upstream returns, with a fallback to &lt;code class=&quot;language-text&quot;&gt;reasoning_content&lt;/code&gt; for the times Qwen answers inside its thinking block.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;turnstile-as-the-abuse-gate&quot;&gt;Turnstile as the Abuse Gate&lt;/h2&gt;
&lt;p&gt;Origin checks stop drive-by scripts, but &lt;code class=&quot;language-text&quot;&gt;Origin&lt;/code&gt; is just a header, and anything outside a browser can forge it. Turnstile is the piece that can&apos;t be forged, and it&apos;s wired as a two-endpoint flow so the widget only runs occasionally instead of on every request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client side&lt;/strong&gt;, an invisible widget is rendered off-screen once and executed on demand (&lt;code class=&quot;language-text&quot;&gt;src/lib/turnstile.js&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;widgetId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;turnstile&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;host&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;sitekey&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SITEKEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;flexible&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;share&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pendingResolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; pendingResolver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// later:&lt;/span&gt;
window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;turnstile&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;widgetId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;share&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The token goes to &lt;code class=&quot;language-text&quot;&gt;/api/llm-session&lt;/code&gt;, which verifies it server-side and mints a short-lived session:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;verifyTurnstile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;secret&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; token&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; form &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URLSearchParams&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;secret&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; secret&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;response&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ip&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;remoteip&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ip&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://challenges.cloudflare.com/turnstile/v0/siteverify&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; form &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A passing verification returns an HMAC-signed token, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;expiry&gt;.&amp;lt;nonce&gt;.&amp;lt;signature&gt;&lt;/code&gt;, signed with WebCrypto, valid for 10 minutes:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mintToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; exp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;TTL_SECONDS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nonce &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;b64url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; payload &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;exp&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;nonce&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sign&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;secret&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// HMAC-SHA256 via crypto.subtle&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;payload&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;sig&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The shared client (&lt;code class=&quot;language-text&quot;&gt;src/utils/llm.js&lt;/code&gt;) caches that session token, attaches it to every &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt; call as &lt;code class=&quot;language-text&quot;&gt;X-LLM-Session&lt;/code&gt;, and transparently re-mints when it expires or gets a 401. &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt; verifies the signature and expiry on each request. No state, no KV, just math:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;TURNSTILE_SECRET&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_SESSION_SECRET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ok &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;verifySession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;X-LLM-Session&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LLM_SESSION_SECRET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Unauthorized, no valid session&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The chain of custody: you can&apos;t get a session without passing Turnstile, you can&apos;t pass Turnstile without a real browser on an allowed origin, and you can&apos;t call the LLM without a session. A forger with &lt;code class=&quot;language-text&quot;&gt;curl&lt;/code&gt; fails at step one.&lt;/p&gt;
&lt;p&gt;One design decision worth copying: the whole gate &lt;strong&gt;fails open&lt;/strong&gt;. If &lt;code class=&quot;language-text&quot;&gt;TURNSTILE_SECRET&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;LLM_SESSION_SECRET&lt;/code&gt; is unset, &lt;code class=&quot;language-text&quot;&gt;/api/llm-session&lt;/code&gt; hands back a placeholder token and &lt;code class=&quot;language-text&quot;&gt;/api/llm&lt;/code&gt; skips the check. The client flow is identical in dev (where there&apos;s no Turnstile) and prod, and misconfiguring a secret degrades to &quot;no bot gate&quot; instead of &quot;site-wide outage.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;neon-gpt&quot;&gt;Neon GPT&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/tools/chat&quot;&gt;Neon GPT&lt;/a&gt; is the most direct consumer: a no-login chat UI at &lt;code class=&quot;language-text&quot;&gt;/tools/chat&lt;/code&gt; that talks straight through this stack. There&apos;s nothing special about its API usage. It calls the same &lt;code class=&quot;language-text&quot;&gt;askLLM()&lt;/code&gt; helper as everything else:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reply &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;askLLM&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;maxTokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;temperature&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;45000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/blog-images/neon-gpt-chat.png&quot; alt=&quot;Neon GPT chat UI talking to the Qwen endpoint through the AI Gateway&quot;&gt;&lt;/p&gt;
&lt;p&gt;The user never logs in, never sees a CAPTCHA, and never learns the endpoint. The invisible Turnstile run happens on first message, the session rides along for 10 minutes, and re-minting is invisible too. The same helper powers the Blocks world generator and the arcade games, so every AI feature on the site inherits the gate for free.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;design-decisions&quot;&gt;Design Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why an AI Gateway between two hobby systems?&lt;/strong&gt; Because the alternative is my code calling my friend&apos;s box with zero visibility for either of us. The gateway gives both sides an audit trail neither has to build: request logs, token counts, latency, error rates. It&apos;s also a kill switch. If something on my site goes haywire, the gateway can rate-limit or shut off the traffic without him touching his server. And since the gateway is authenticated, even the gateway URL leaking doesn&apos;t expose his endpoint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Turnstile instead of accounts?&lt;/strong&gt; The features are toys: a chat page and some arcade games. Forcing login would kill them, and standing up auth infrastructure to protect a friend&apos;s GPU is disproportionate. Turnstile is free, invisible in this configuration, and moves the cost of abuse from &quot;free curl loop&quot; to &quot;defeating a browser challenge every 10 minutes.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why the HMAC session layer instead of verifying Turnstile per request?&lt;/strong&gt; Turnstile tokens are single-use, and running the widget before every LLM call would add latency to each message. Exchanging one Turnstile pass for a 10-minute signed session amortizes the challenge across a whole play session. Verification is a stateless HMAC check: no session store, no KV reads, just &lt;code class=&quot;language-text&quot;&gt;crypto.subtle.verify&lt;/code&gt; in the function.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a Pages Function at all, when the AI Gateway is already a proxy?&lt;/strong&gt; Keys. The browser can&apos;t hold the LiteLLM key or the gateway token, so &lt;em&gt;something&lt;/em&gt; server-side has to inject them. Once that function exists, it&apos;s also the right place for origin checks, input caps, and response normalization, things the gateway doesn&apos;t know about my app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why LiteLLM on the far end?&lt;/strong&gt; It makes the local model boring. Qwen behind LiteLLM speaks the same protocol as everything else, so if the box is down, swapping &lt;code class=&quot;language-text&quot;&gt;LLM_GATEWAY_URL&lt;/code&gt; to any other OpenAI-compatible endpoint is a config change, not a code change.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The whole thing is two Pages Functions, one client helper, one dashboard gateway, and zero servers on my side. A public static site gets LLM features backed by a friend&apos;s local model, with the keys server-side, the traffic observable by both parties, and the abuse surface reduced to &quot;must be a real browser, on my site, at most this fast, at most this big.&quot;&lt;/p&gt;
&lt;p&gt;If you&apos;re fronting any self-hosted model for public use, the pattern generalizes: OpenAI-compatible upstream, AI Gateway for observability and the kill switch, a thin function for secrets and caps, and Turnstile exchanged for a signed session so the challenge cost is paid once per session instead of per request.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenCode Part 1: A Terminal AI Agent You Actually Control]]></title><description><![CDATA[OpenCode is an open source AI coding agent with provider flexibility, cross-repo context, and per-session cost tracking. Here's how to set it up and why GitHub's pricing changes make it worth a look.]]></description><link>https://chrishouse.io/opencode-part-1/</link><guid isPermaLink="false">https://chrishouse.io/opencode-part-1/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This is Part 1 of a series on self-hosting an LLM. &lt;a href=&quot;/cloudflare-ai-gateway-turnstile&quot;&gt;Part 2&lt;/a&gt; fronts a friend&apos;s local Qwen for a public site, &lt;a href=&quot;/self-hosted-llm-cloudflare-tunnel-ollama&quot;&gt;Part 3&lt;/a&gt; runs your own model on a Cloudflare Tunnel, and &lt;a href=&quot;/redis-agent-memory-server-ollama&quot;&gt;Part 4&lt;/a&gt; gives it a memory with Redis.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GitHub Copilot&apos;s pricing has gotten complicated. What used to be a straightforward $10/month individual subscription has expanded into a tiered structure — Pro, Pro+, Max — ranging from $10 to $100/month, all moving toward a credit-based consumption model where heavy usage costs more on top of the base plan. New self-serve signups for Copilot Business were even &lt;a href=&quot;https://docs.github.com/en/copilot/get-started/plans&quot;&gt;temporarily paused in April 2026&lt;/a&gt;. On the enterprise side, if no user-level budget is configured, individual developers have no visibility into what they&apos;re personally consuming — only org and enterprise admins can see aggregate usage through the billing dashboard. You can be burning credits on long agent sessions with no idea what it&apos;s costing until the invoice lands.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://opencode.ai/&quot;&gt;OpenCode&lt;/a&gt; is an open source AI coding agent that runs in your terminal — and it&apos;s a reasonable answer to that problem. You bring your own model, and it tracks token usage and cost against every session locally. For custom or self-hosted providers you define your own per-token pricing. GitHub officially added Copilot subscription support for it earlier this year, so if you already have a paid Copilot plan you can &lt;a href=&quot;https://github.blog/changelog/2026-01-16-github-copilot-now-supports-opencode/&quot;&gt;authenticate into OpenCode with those credentials&lt;/a&gt; and use it without any additional setup.&lt;/p&gt;
&lt;p&gt;Here&apos;s how I have it configured.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-config&quot;&gt;The Config&lt;/h2&gt;
&lt;p&gt;OpenCode lives at &lt;code class=&quot;language-text&quot;&gt;~/.config/opencode/opencode.jsonc&lt;/code&gt;. Everything — providers, models, plugins, permissions, MCP servers — is in one file.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,

  // Cross-repo references — use @alias in any session to bring in context
  &amp;quot;reference&amp;quot;: {
    &amp;quot;api&amp;quot;:          { &amp;quot;path&amp;quot;: &amp;quot;~/dev/my-api&amp;quot; },
    &amp;quot;infra&amp;quot;:        { &amp;quot;path&amp;quot;: &amp;quot;~/dev/my-infra&amp;quot; },
    &amp;quot;shared-libs&amp;quot;:  { &amp;quot;path&amp;quot;: &amp;quot;~/dev/shared-libs&amp;quot; }
  },

  &amp;quot;lsp&amp;quot;: true,
  &amp;quot;formatter&amp;quot;: true,
  &amp;quot;share&amp;quot;: &amp;quot;disabled&amp;quot;,
  &amp;quot;snapshot&amp;quot;: true,

  &amp;quot;provider&amp;quot;: {
    &amp;quot;friends-gateway&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;AI Gateway&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;https://llm.azure_fakeness.app/v1&amp;quot;,
        &amp;quot;apiKey&amp;quot;: &amp;quot;&amp;lt;api-key&amp;gt;&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;claude-sonnet-4-5&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;Claude Sonnet (gateway)&amp;quot;,
          &amp;quot;cost&amp;quot;: {
            &amp;quot;input&amp;quot;: 3.00,
            &amp;quot;output&amp;quot;: 15.00,
            &amp;quot;cache_read&amp;quot;: 0.30,
            &amp;quot;cache_write&amp;quot;: 3.75
          },
          &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 200000, &amp;quot;output&amp;quot;: 16000 }
        }
      }
    },
    &amp;quot;local&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;Ollama (local)&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;http://127.0.0.1:11434/v1&amp;quot;,
        &amp;quot;apiKey&amp;quot;: &amp;quot;ollama&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;qwen3.6:27b-mlx&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;Qwen3.6 27B MLX (local)&amp;quot;,
          &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 128000, &amp;quot;output&amp;quot;: 8192 }
        }
      }
    }
  },

  &amp;quot;model&amp;quot;: &amp;quot;friends-gateway/claude-sonnet-4-5&amp;quot;,

  &amp;quot;plugin&amp;quot;: [&amp;quot;opencode-vibeguard&amp;quot;, &amp;quot;@tarquinen/opencode-dcp&amp;quot;],

  &amp;quot;permission&amp;quot;: {
    &amp;quot;bash&amp;quot;: &amp;quot;ask&amp;quot;,
    &amp;quot;edit&amp;quot;: &amp;quot;ask&amp;quot;,
    &amp;quot;webfetch&amp;quot;: &amp;quot;ask&amp;quot;,
    &amp;quot;read&amp;quot;: &amp;quot;allow&amp;quot;,
    &amp;quot;glob&amp;quot;: &amp;quot;allow&amp;quot;,
    &amp;quot;grep&amp;quot;: &amp;quot;allow&amp;quot;
  },

  &amp;quot;mcp&amp;quot;: {
    &amp;quot;github&amp;quot;: {
      &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://api.githubcopilot.com/mcp/&amp;quot;
    },
    &amp;quot;your-observability-tool&amp;quot;: {
      &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://mcp.yourtool.com/mcp&amp;quot;,
      &amp;quot;headers&amp;quot;: {
        &amp;quot;Authorization&amp;quot;: &amp;quot;Bearer &amp;lt;token&amp;gt;&amp;quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cross-repo-references&quot;&gt;Cross-Repo References&lt;/h2&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;reference&lt;/code&gt; block lets you give repos an alias and pull them in by name during any session:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;how does @infra handle network rules between environments?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;compare how @api and @shared-libs both handle auth middleware&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;OpenCode treats the referenced repo as additional context for that session. You can ask questions that span multiple codebases without switching directories or opening separate sessions. For any project where work is spread across repos, this alone is worth the setup.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;providers--including-a-friends-gateway&quot;&gt;Providers — Including a Friend&apos;s Gateway&lt;/h2&gt;
&lt;p&gt;OpenCode uses the &lt;a href=&quot;https://sdk.vercel.ai/&quot;&gt;AI SDK&lt;/a&gt; under the hood, so any OpenAI-compatible endpoint works as a provider. That includes hosted APIs from Anthropic, OpenAI, and Google — but also any custom gateway someone is running.&lt;/p&gt;
&lt;p&gt;If you have a generous friend hosting a model AI gateway on a Blackwell 6000 they have laying around, you can point OpenCode at it the same way you&apos;d point it at any API:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;&amp;quot;friends-gateway&amp;quot;: {
  &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;Friend&amp;#39;s Gateway&amp;quot;,
  &amp;quot;options&amp;quot;: {
    &amp;quot;baseURL&amp;quot;: &amp;quot;https://llm.feverdreams.app/v1&amp;quot;,
    &amp;quot;apiKey&amp;quot;: &amp;quot;&amp;lt;their-key&amp;gt;&amp;quot;
  },
  &amp;quot;models&amp;quot;: {
    &amp;quot;claude-sonnet-4-5&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;Claude Sonnet (gateway)&amp;quot;,
      &amp;quot;cost&amp;quot;: { &amp;quot;input&amp;quot;: 3.00, &amp;quot;output&amp;quot;: 15.00 },
      &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 200000, &amp;quot;output&amp;quot;: 16000 }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;cost&lt;/code&gt; values are whatever you&apos;ve agreed to pay — or zero if they&apos;re sharing access. OpenCode doesn&apos;t validate pricing against any external source, it just multiplies tokens by whatever you define. Which is the whole point.&lt;/p&gt;
&lt;p&gt;The local Ollama provider works the same way — same config pattern, just pointed at &lt;code class=&quot;language-text&quot;&gt;localhost&lt;/code&gt;. Part 2 covers what&apos;s worth running locally on Apple Silicon.&lt;/p&gt;
&lt;p&gt;Switching providers mid-session is a keybind. No restart needed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;plugins&quot;&gt;Plugins&lt;/h2&gt;
&lt;p&gt;Two plugins worth knowing about:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/vibeguard/opencode-vibeguard&quot;&gt;opencode-vibeguard&lt;/a&gt;&lt;/strong&gt; — scans outgoing prompts for secrets, JWTs, API keys, and credentials before they leave. Any match gets replaced with a placeholder before the request goes out. Useful if you work in a codebase where sensitive values end up in config files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/Tarquinen/opencode-dynamic-context-pruning&quot;&gt;@tarquinen/opencode-dcp&lt;/a&gt;&lt;/strong&gt; — dynamic context pruning. Compresses and prunes context as sessions grow so you don&apos;t hit the limit mid-conversation on a long debugging session. The cache read numbers in the usage table below are large partly because of this plugin keeping context alive across turns efficiently.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mcp-servers&quot;&gt;MCP Servers&lt;/h2&gt;
&lt;p&gt;MCP (Model Context Protocol) servers let OpenCode call external tools mid-session — querying your observability platform, pulling issue data, reading a service catalog, checking deployment status. Most major platforms have MCP servers now: GitHub, Linear, Jira, Datadog, PagerDuty, Grafana, Notion, Slack. If it has an API, there&apos;s probably an MCP server for it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cost-tracking&quot;&gt;Cost Tracking&lt;/h2&gt;
&lt;p&gt;Every session&apos;s token usage and cost gets written to a local SQLite database at &lt;code class=&quot;language-text&quot;&gt;~/.local/share/opencode/opencode.db&lt;/code&gt;. For custom providers, the &lt;code class=&quot;language-text&quot;&gt;cost&lt;/code&gt; field reflects whatever per-token pricing you defined in the config — OpenCode multiplies tokens by your rates, no external validation.&lt;/p&gt;
&lt;p&gt;For GitHub Copilot specifically: token counts are real, pulled from the API response. The dollar figure is OpenCode&apos;s own estimate using Anthropic&apos;s public per-token pricing via the AI SDK — it is not GitHub&apos;s AIC (AI Credits). If you want to know your actual AIC consumption, that&apos;s still in the GitHub billing dashboard. What OpenCode gives you is a relative measure — how many tokens a session consumed and roughly what that would cost at list price. Good enough for spotting runaway sessions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If GitHub&apos;s AIC is coming out higher than OpenCode&apos;s estimate, that&apos;s the signal. OpenCode is calculating against Anthropic&apos;s public list price — if Copilot costs more than that for the same tokens, you&apos;re paying a premium for the subscription wrapper. A direct gateway gives you both numbers to compare.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wrote a small Python script that reads that database and outputs a markdown summary. It runs via a custom &lt;code class=&quot;language-text&quot;&gt;/usage&lt;/code&gt; command:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Show token and cost usage stats&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
!&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`python3 ~/bin/opencode-usage-md`&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Type &lt;code class=&quot;language-text&quot;&gt;/usage&lt;/code&gt; in any session:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;In Tokens&lt;/th&gt;
&lt;th&gt;Out Tokens&lt;/th&gt;
&lt;th&gt;Cache Read&lt;/th&gt;
&lt;th&gt;Cache Write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Today&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$0.17&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;2,074&lt;/td&gt;
&lt;td&gt;103,049&lt;/td&gt;
&lt;td&gt;28,093&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;This Week&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;$10.96&lt;/td&gt;
&lt;td&gt;966&lt;/td&gt;
&lt;td&gt;120,717&lt;/td&gt;
&lt;td&gt;16,109,350&lt;/td&gt;
&lt;td&gt;1,150,924&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;This Month&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;$24.47&lt;/td&gt;
&lt;td&gt;362,404&lt;/td&gt;
&lt;td&gt;278,251&lt;/td&gt;
&lt;td&gt;27,766,983&lt;/td&gt;
&lt;td&gt;3,320,768&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All Time&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;$28.79&lt;/td&gt;
&lt;td&gt;364,598&lt;/td&gt;
&lt;td&gt;568,755&lt;/td&gt;
&lt;td&gt;86,375,370&lt;/td&gt;
&lt;td&gt;8,588,282&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And the most recent sessions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Out&lt;/th&gt;
&lt;th&gt;Cache Read&lt;/th&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;06-24 17:26&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;5,724&lt;/td&gt;
&lt;td&gt;115,965&lt;/td&gt;
&lt;td&gt;Explore React router changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06-24 15:53&lt;/td&gt;
&lt;td&gt;$3.68&lt;/td&gt;
&lt;td&gt;37,506&lt;/td&gt;
&lt;td&gt;6,403,169&lt;/td&gt;
&lt;td&gt;Security Scan and fix.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06-24 15:34&lt;/td&gt;
&lt;td&gt;$1.81&lt;/td&gt;
&lt;td&gt;24,614&lt;/td&gt;
&lt;td&gt;2,606,697&lt;/td&gt;
&lt;td&gt;Incident write-up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The script is straightforward — a few SQL queries against the local database:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; sqlite3
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; datetime &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; datetime
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pathlib &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Path

DB &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;home&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.local/share/opencode/opencode.db&quot;&lt;/span&gt;
conn &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sqlite3&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connect&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DB&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; conn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cursor&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;agg&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;since_ms&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;execute&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;SELECT COUNT(*) n, SUM(cost) cost, SUM(tokens_input) ti, SUM(tokens_output) to_, &quot;&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;SUM(tokens_cache_read) cr, SUM(tokens_cache_write) cw FROM session WHERE time_updated &gt;= ?&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;since_ms&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fetchone&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;session&lt;/code&gt; table has everything. Cost by project, week-over-week trends, alerting when a session goes over a threshold — it&apos;s all queryable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; opencode&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you have a Copilot subscription and want to try it first:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/connect&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Select &lt;strong&gt;GitHub Copilot&lt;/strong&gt;, complete the device login, done. If you want to use your own provider, add it to &lt;code class=&quot;language-text&quot;&gt;~/.config/opencode/opencode.jsonc&lt;/code&gt; and switch models with &lt;code class=&quot;language-text&quot;&gt;Ctrl+M&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Part 2 covers running Ollama locally, which models are worth running on Apple Silicon, and how the local provider hooks into the same &lt;code class=&quot;language-text&quot;&gt;/usage&lt;/code&gt; tracking — except the cost column reads $0.00.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GitHub's Agentic Workflows]]></title><description><![CDATA[GitHub Agentic Workflows let you describe repo tasks in plain Markdown and have a coding agent run them on a schedule. Here's how they work and what you can do with them.]]></description><link>https://chrishouse.io/github-agentic-workflows/</link><guid isPermaLink="false">https://chrishouse.io/github-agentic-workflows/</guid><pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There&apos;s a category of work that lives in every repo that nobody actually wants to do. Updating the README after the API changes. Fixing twelve broken links that crept in over six months. Regenerating the architecture slides before the quarterly review. It&apos;s not hard. It&apos;s just relentless and invisible until someone notices it&apos;s wrong.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.github.com/gh-aw/&quot;&gt;GitHub Agentic Workflows&lt;/a&gt; are GitHub&apos;s answer to that. They&apos;re currently in public preview.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-they-are&quot;&gt;What They Are&lt;/h2&gt;
&lt;p&gt;Regular GitHub Actions automation is deterministic. You define steps, a runner executes them, done. It doesn&apos;t reason—it does what you told it.&lt;/p&gt;
&lt;p&gt;Agentic workflows are Markdown files that live in &lt;code class=&quot;language-text&quot;&gt;.github/workflows/&lt;/code&gt; alongside your regular YAML. Instead of scripted steps, you describe what you want in natural language. When the workflow runs, a coding agent—Copilot CLI, Claude Code, or OpenAI Codex—reads the repo, figures out what to do, and opens a PR.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Traditional Workflow                Agentic Workflow
─────────────────────────           ──────────────────────────
Trigger → Fixed Steps → Done        Trigger → Agent reads repo
                                               Agent forms plan
                                               Agent edits files
                                               Agent runs tests
                                               Agent opens PR
                                               You review + merge&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The format is a Markdown file with a YAML frontmatter block. Frontmatter handles the trigger, permissions, tools, and what the agent is allowed to output. The Markdown body is the instruction:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; daily

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;issues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-issue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[report] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Daily Repo Status Report&lt;/span&gt;

Create a daily status report for maintainers. Include recent activity,
open PRs worth reviewing, and any CI failures that need attention.
Keep it concise and link to the relevant issues/PRs.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;safe-outputs&lt;/code&gt; block is what keeps it from going rogue. Write operations—creating PRs, posting comments, opening issues—have to be explicitly listed. Anything not in that list the agent can&apos;t do. PRs are never auto-merged.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;setting-one-up&quot;&gt;Setting One Up&lt;/h2&gt;
&lt;p&gt;GitHub Agentic Workflows are in &lt;a href=&quot;https://github.github.com/gh-aw/&quot;&gt;public preview&lt;/a&gt;. Once you&apos;re in, install the CLI extension:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh extension &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; github/gh-aw&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The easiest way to write your first workflow is to ask a coding agent to do it:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Generate a workflow that checks for broken links in my docs weekly.
Use the instructions at https://github.com/github/gh-aw/blob/main/create.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It drafts the Markdown file and a corresponding &lt;code class=&quot;language-text&quot;&gt;.lock.yml&lt;/code&gt; that GitHub Actions actually executes. Review both, then compile and push:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh aw compile
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; .github/workflows/check-links.md .github/workflows/check-links.lock.yml
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;add: broken link check workflow&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;agentsmd&quot;&gt;AGENTS.md&lt;/h3&gt;
&lt;p&gt;Drop an &lt;code class=&quot;language-text&quot;&gt;AGENTS.md&lt;/code&gt; in the repo root. The agent reads it at the start of every task—what it can touch, what it can&apos;t, any style rules:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; AGENTS.md&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; What you can change&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`docs/`&lt;/span&gt; — All Markdown documentation
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`README.md`&lt;/span&gt; — Top-level readme
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`CHANGELOG.md`&lt;/span&gt; — Add entries, never modify existing ones

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; What you must not touch&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`environments/`&lt;/span&gt; — Terraform configs, changes here deploy to production
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`.github/workflows/`&lt;/span&gt; — Workflow changes require human review

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Style&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Sentence case for headings
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; External links must use HTTPS
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Code examples must be runnable before including them&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is also where structured documentation frameworks like &lt;a href=&quot;https://diataxis.fr/&quot;&gt;Diataxis&lt;/a&gt; pay off. If your docs follow a consistent structure—tutorials, how-tos, reference, explanation—you can tell the agent exactly which sections are safe to update automatically. Reference tables: fine. Conceptual explanation: probably leave that to a human.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;scheduling&quot;&gt;Scheduling&lt;/h2&gt;
&lt;p&gt;Any trigger GitHub Actions supports works in the frontmatter:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0 9 * * 1&apos;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Every Monday at 9am&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;base-branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;documentation&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;github&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  shell&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Weekly Documentation Refresh&lt;/span&gt;

Review all documentation in &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`docs/`&lt;/span&gt; for:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Broken or outdated external links
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Version numbers that don&apos;t match &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`package.json`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; API endpoints or config keys that no longer exist in the codebase

Fix what&apos;s mechanical. Flag anything requiring judgment with a
&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`&amp;lt;!-- TODO: verify --&gt;`&lt;/span&gt; comment. Open a pull request with all changes
and a summary of what was updated and why.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;some-things-worth-building&quot;&gt;Some Things Worth Building&lt;/h2&gt;
&lt;h3 id=&quot;broken-link-repair&quot;&gt;Broken Link Repair&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0 8 * * 0&apos;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;documentation&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Broken Link Repair&lt;/span&gt;

Install markdown-link-check. Scan all Markdown files for broken external links.
For each broken link: find the new URL if the content moved, remove the hyperlink
but keep the text if the page is gone, or add a &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- TODO: verify --&gt;&lt;/span&gt; comment if
the entire reference is outdated. Open a pull request with all fixes.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;documentation-freshness&quot;&gt;Documentation Freshness&lt;/h3&gt;
&lt;p&gt;Docs drift. API docs reference endpoints that were renamed. Setup guides reference package versions from two releases back. Nobody notices until someone follows the guide and it breaks.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0 9 * * 1&apos;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;documentation&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Documentation Freshness Check&lt;/span&gt;

Review all documentation in docs/ against the current codebase. Find anywhere
the docs describe behavior, APIs, or configuration that no longer matches the
actual code. Update the docs to match. Do not rewrite prose—only fix factual
inaccuracies. Include a list of every change with before/after.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If your docs follow &lt;a href=&quot;https://diataxis.fr/&quot;&gt;Diataxis&lt;/a&gt;—tutorials, how-tos, reference, explanation—you can scope it to just the reference section and keep the risk low.&lt;/p&gt;
&lt;h3 id=&quot;powerpoint-updates&quot;&gt;PowerPoint Updates&lt;/h3&gt;
&lt;p&gt;The agent can edit &lt;code class=&quot;language-text&quot;&gt;.pptx&lt;/code&gt; files directly using &lt;code class=&quot;language-text&quot;&gt;python-pptx&lt;/code&gt;. Point it at your architecture deck and the current infrastructure config and it&apos;ll update slide text to match.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;slides&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Architecture Slide Refresh&lt;/span&gt;

Install python-pptx. Open &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`docs/slides/architecture.pptx`&lt;/span&gt; and read the current
slide content. Compare service names, topology, and component labels against the
current infrastructure configuration in &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`environments/`&lt;/span&gt;.

For each slide where the content no longer matches reality:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Update service names, counts, and connection labels to match current config
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Do not change layout, styling, or colors—only text content

Save the updated file. Open a PR with before/after notes for each changed slide.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;changelog-generation&quot;&gt;Changelog Generation&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;changelog&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Changelog Generation&lt;/span&gt;

Look at the git log since the last tagged release. Group commits by type:
features, fixes, breaking changes, internal. Generate a new CHANGELOG.md entry
following the existing format. Do not modify any previous entries.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;security-advisory-response&quot;&gt;Security Advisory Response&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;issues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;opened&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;issues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[security] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;security&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;add-issue-comment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Security Advisory Response&lt;/span&gt;

A security advisory has been filed. Review whether this repo uses the affected
package and the affected API surface. If affected: describe the exposure in
plain language, propose a fix, implement it if mechanical, open a PR with a
security label. If not affected, comment on the issue with an explanation.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;readme-hygiene&quot;&gt;README Hygiene&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0 9 * * 1&apos;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;safe-outputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create-pull-request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title-prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[automated] &quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;documentation&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; automated&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; README Hygiene&lt;/span&gt;

Review README.md against the current state of the repo. Update the getting started
section to match current setup steps, fix any command-line examples that no longer
work, and update the architecture overview if the major components have changed.
Do not change the introduction, contributing guidelines, or overall structure.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-it-falls-down&quot;&gt;Where It Falls Down&lt;/h2&gt;
&lt;p&gt;It can&apos;t reach your production systems or query live observability data unless you wire that up via tools in the frontmatter. It&apos;ll make confident decisions on open-ended architecture questions, which isn&apos;t always what you want. And the vaguer the instruction, the more variable the output—same as any AI task.&lt;/p&gt;
&lt;p&gt;The more specific the instruction and the more bounded the scope, the more predictable the result.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Platform AI Part 4: Building a Claude Code Plugin to Develop the AI Itself]]></title><description><![CDATA[How I built a Claude Code skill that turns my IDE into a multi-agent development studio for building, debugging, and shipping features on my AI DevOps assistant.]]></description><link>https://chrishouse.io/platform-ai-dev-studio/</link><guid isPermaLink="false">https://chrishouse.io/platform-ai-dev-studio/</guid><pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This is Part 4 of the Platform AI series. &lt;a href=&quot;/blog/building-ai-chatbot-claude-cloudflare&quot;&gt;Part 1&lt;/a&gt; covers building the basic chatbot. &lt;a href=&quot;/blog/platform-ai-assistant&quot;&gt;Part 2&lt;/a&gt; adds agentic tool use. &lt;a href=&quot;/blog/platform-ai-multi-agent&quot;&gt;Part 3&lt;/a&gt; introduces the multi-agent system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The AI assistant from the first three parts of this series has grown into a real system. Six specialized agents, a hallucination guard with four verification layers, 81 end-to-end tests, tool retry logic, parallel execution, budget tracking, and a self-evaluation pipeline. Making changes to it now requires understanding how all those pieces fit together.&lt;/p&gt;
&lt;p&gt;I found myself repeating the same workflow every time I needed to fix a bug or add a feature. Export a chat session from the assistant to see what went wrong. Read through the codebase to find the relevant files. Make changes. Run the e2e suite. Check for regressions. That process is exactly the kind of structured, multi-step workflow that AI excels at.&lt;/p&gt;
&lt;p&gt;So I built a Claude Code plugin to do it for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href=&quot;https://chrishouse.io/tools/ai&quot;&gt;https://chrishouse.io/tools/ai&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-a-claude-code-skill&quot;&gt;What Is a Claude Code Skill?&lt;/h2&gt;
&lt;p&gt;Claude Code is Anthropic&apos;s CLI tool for using Claude directly in your terminal. It reads your codebase, edits files, runs commands, and understands project context. But out of the box, it&apos;s general-purpose. It doesn&apos;t know that my project has a specific agent architecture, that tests live in &lt;code class=&quot;language-text&quot;&gt;e2e/&lt;/code&gt;, or that every async function needs try/catch with bracket-prefixed logging.&lt;/p&gt;
&lt;p&gt;Skills (also called slash commands) let you inject domain-specific expertise into Claude Code. A skill is a markdown file that acts as a system prompt, activated by typing &lt;code class=&quot;language-text&quot;&gt;/skill-name&lt;/code&gt; in the CLI. When you invoke a skill, Claude Code loads that markdown as additional context, effectively turning a general-purpose AI into a specialist for your project.&lt;/p&gt;
&lt;p&gt;The skill lives at &lt;code class=&quot;language-text&quot;&gt;.claude/commands/ai-dev-studio.md&lt;/code&gt; in the repo. There&apos;s also a plugin registration that tells Claude Code this skill exists:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai-dev-studio&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AI Development Studio - Multi-agent system for building and improving the AI DevOps assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Chris House&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chris@platformlab.dev&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-ai-dev-studio&quot;&gt;The AI Dev Studio&lt;/h2&gt;
&lt;p&gt;The skill is called &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt; and it transforms Claude Code into a six-agent development team. Each agent has a specific role in the development lifecycle:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;User Request
     │
     ▼
┌─────────────┐
│  Classify    │ ── New Feature? Bug Fix? Improvement? Review?
└─────────────┘
     │
     ▼
┌─────────────────────────────────────────────────┐
│                Agent Pipeline                    │
│                                                  │
│  ARCHITECT → DEVELOPER → QA → REVIEWER → PRODUCT│
│                                                  │
│  Bug fix:    DEBUGGER → DEVELOPER → QA           │
│  Improvement: REVIEWER → DEVELOPER → QA          │
└─────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When I type &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt; and paste a chat session export or describe a bug, the skill classifies the request and activates the right agents in sequence.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-six-agents&quot;&gt;The Six Agents&lt;/h2&gt;
&lt;h3 id=&quot;architect&quot;&gt;Architect&lt;/h3&gt;
&lt;p&gt;Architect handles design work. When I ask it to add a new feature, it reads the existing codebase first, studies how similar features are built, then produces a blueprint with specific file paths, line numbers, data flows, and integration points. It knows about the &lt;code class=&quot;language-text&quot;&gt;BaseAgent&lt;/code&gt; pattern, the agent orchestrator, the tool definition conventions, and the module structure.&lt;/p&gt;
&lt;h3 id=&quot;developer&quot;&gt;Developer&lt;/h3&gt;
&lt;p&gt;Developer writes the actual code. It follows the existing conventions exactly because the skill prompt includes specific code style examples from the codebase. Named functions instead of arrow functions. Try/catch with bracket-prefixed console logs like &lt;code class=&quot;language-text&quot;&gt;[ModuleName]&lt;/code&gt;. Zod schemas for tool inputs. The skill enforces these patterns so the output matches the rest of the codebase without manual cleanup.&lt;/p&gt;
&lt;h3 id=&quot;qa&quot;&gt;QA&lt;/h3&gt;
&lt;p&gt;QA writes Playwright e2e tests and runs them. The skill includes the exact test fixture pattern from &lt;code class=&quot;language-text&quot;&gt;e2e/fixtures.js&lt;/code&gt; so QA knows to call &lt;code class=&quot;language-text&quot;&gt;clearBrowserState()&lt;/code&gt; first, use &lt;code class=&quot;language-text&quot;&gt;aiAssistant.sendMessage()&lt;/code&gt; with appropriate timeouts, and check for both positive results and error states. After writing tests, it runs them and reports results.&lt;/p&gt;
&lt;h3 id=&quot;debugger&quot;&gt;Debugger&lt;/h3&gt;
&lt;p&gt;Debugger is the investigator. When a test fails or a session export shows unexpected behavior, Debugger reads the error, traces the execution path through the code, forms hypotheses ranked by confidence, and proposes minimal fixes. It uses a structured format with root cause analysis, evidence, and validation steps.&lt;/p&gt;
&lt;h3 id=&quot;reviewer&quot;&gt;Reviewer&lt;/h3&gt;
&lt;p&gt;Reviewer checks for code quality, security, DRY violations, and convention adherence. It produces prioritized findings: P0 (must fix), P1 (should fix), P2 (nice to have). The skill includes specific checklists for pattern adherence, error handling, security (input validation, injection risks), and code quality.&lt;/p&gt;
&lt;h3 id=&quot;product&quot;&gt;Product&lt;/h3&gt;
&lt;p&gt;Product is the final quality gate. It reviews user-facing elements like error messages, success states, and response formatting against what the skill calls &quot;Apple-level quality standards.&quot; The idea is that every user-visible string should be clear, actionable, and helpful rather than generic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-session-export-workflow&quot;&gt;The Session Export Workflow&lt;/h2&gt;
&lt;p&gt;The most powerful pattern is feeding the plugin exported chat sessions from the AI assistant itself. The assistant&apos;s frontend has an export button that dumps the full session as JSON, including every message, tool call, tool result, agent routing decision, thinking blocks, and warnings.&lt;/p&gt;
&lt;p&gt;When I paste that export into &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt;, the Debugger agent can trace exactly what happened:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;User asked: &quot;check my clusters status&quot;
  → Triage agent selected (confidence: 0.70)
  → get_cluster_status called → ERROR (4 times)
  → Model generated response with cluster data table
  → No fabrication warning shown

Problem: All tools failed but the model fabricated data&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is how I found and fixed the fabrication detector issues. The session showed that &lt;code class=&quot;language-text&quot;&gt;get_cluster_status&lt;/code&gt; failed four times with timeouts, but the assistant still presented a complete table with cluster names, regions, versions, and node counts. All fabricated. The Debugger agent traced the issue through the hallucination guard&apos;s four verification layers, identified why the detection missed it (markdown tables aren&apos;t code blocks, so the config-pattern matching didn&apos;t trigger), and proposed the exact fix.&lt;/p&gt;
&lt;p&gt;In a later session, the IaC agent gave legitimate YAML suggestions for improving ArgoCD configurations, but the fabrication detector flagged them as hallucinations because no tools were called. The Debugger traced that false positive to the &lt;code class=&quot;language-text&quot;&gt;processHallucinationGuard&lt;/code&gt; function where the logic assumed &quot;no tools + YAML = fabrication&quot; without considering that advisory agents work from pre-loaded context.&lt;/p&gt;
&lt;p&gt;Both fixes, across three files, came from the same workflow: export session, paste into &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt;, let the agents trace the problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-it-knows-the-codebase&quot;&gt;How It Knows the Codebase&lt;/h2&gt;
&lt;p&gt;The skill prompt is 600 lines of markdown that encodes the project&apos;s architecture, conventions, and patterns. It includes the agent capability system:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; AgentCapability &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;READ_ONLY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;read_only&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// Triage, Advisor&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;SUGGEST&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;suggest&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// Suggestions requiring confirmation&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;EXECUTE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;execute&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// Operator - confirmation required&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;AUTONOMOUS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;autonomous&apos;&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// Rare&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;BaseAgent&lt;/code&gt; class pattern showing how to define tools, &lt;code class=&quot;language-text&quot;&gt;canHandle&lt;/code&gt;, system prompts, and handoff logic. The &lt;code class=&quot;language-text&quot;&gt;DynamicStructuredTool&lt;/code&gt; pattern for defining new tools with Zod schemas. The e2e test fixture pattern. And the meta-tool pattern for composite operations like &lt;code class=&quot;language-text&quot;&gt;diagnose_pod_crash&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This context means Claude Code doesn&apos;t have to rediscover these patterns every time. When the Developer agent writes a new agent, it follows the exact pattern from the skill. When QA writes tests, it uses the exact fixture API.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;request-classification&quot;&gt;Request Classification&lt;/h2&gt;
&lt;p&gt;The skill routes requests to different agent pipelines based on keywords:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request Type&lt;/th&gt;
&lt;th&gt;Keywords&lt;/th&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New Feature&lt;/td&gt;
&lt;td&gt;&quot;add&quot;, &quot;create&quot;, &quot;build&quot;&lt;/td&gt;
&lt;td&gt;Architect, Developer, QA, Reviewer, Product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug Fix&lt;/td&gt;
&lt;td&gt;&quot;fix&quot;, &quot;broken&quot;, &quot;error&quot;&lt;/td&gt;
&lt;td&gt;Debugger, Developer, QA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Improvement&lt;/td&gt;
&lt;td&gt;&quot;improve&quot;, &quot;optimize&quot;, &quot;refactor&quot;&lt;/td&gt;
&lt;td&gt;Reviewer, Developer, QA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Coverage&lt;/td&gt;
&lt;td&gt;&quot;test&quot;, &quot;e2e&quot;, &quot;playwright&quot;&lt;/td&gt;
&lt;td&gt;QA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;&quot;design&quot;, &quot;architect&quot;, &quot;plan&quot;&lt;/td&gt;
&lt;td&gt;Architect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Review&lt;/td&gt;
&lt;td&gt;&quot;review&quot;, &quot;check&quot;, &quot;quality&quot;&lt;/td&gt;
&lt;td&gt;Reviewer, Product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This mirrors how the AI assistant itself routes requests to its own agents. There&apos;s something recursive about an AI development tool that uses the same multi-agent routing pattern as the AI it&apos;s building.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;context-preservation-between-agents&quot;&gt;Context Preservation Between Agents&lt;/h2&gt;
&lt;p&gt;When agents hand off to each other, the skill specifies a structured handoff format:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Handoff: Debugger → Developer&lt;/span&gt;

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Work Completed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Traced fabrication detection through 4 layers of hallucination guard
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Identified false positive trigger at response.js:135

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Key Findings&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Advisory agents (iac, advisor) legitimately produce YAML without tools
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; The &quot;no tools + config = fabrication&quot; heuristic is too aggressive

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Root cause analysis with file:line references

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Next Steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;:
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Add agent type exemption for advisory agents
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Broaden suggestion detection patterns in guard.js&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means the Developer agent gets exactly what it needs to implement the fix without re-reading the entire codebase. The Debugger already did the investigation and narrowed down the specific lines that need to change.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;quality-gates&quot;&gt;Quality Gates&lt;/h2&gt;
&lt;p&gt;Each agent phase has a quality gate before handing off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architect&lt;/strong&gt;: Blueprint must be complete with specific file paths and line numbers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer&lt;/strong&gt;: Code must parse without errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QA&lt;/strong&gt;: Tests must be valid and pass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugger&lt;/strong&gt;: Root cause must have supporting evidence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviewer&lt;/strong&gt;: All P0 issues must be addressed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product&lt;/strong&gt;: User-facing strings must be clear and actionable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a gate fails, the pipeline loops back. QA failure goes to Debugger for diagnosis, then back to Developer for the fix, then back to QA.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-example-fixing-the-fabrication-detector&quot;&gt;Real Example: Fixing the Fabrication Detector&lt;/h2&gt;
&lt;p&gt;Here&apos;s how a real bug fix flowed through the plugin. I exported a session where the IaC agent&apos;s YAML suggestions triggered a false fabrication warning, and pasted it into &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugger&lt;/strong&gt; analyzed the session export and traced the issue through three code paths:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;response.js:135&lt;/code&gt; - The &quot;no tools + suspicious content&quot; check didn&apos;t account for advisory agents&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;guard.js:384-389&lt;/code&gt; - The suggestion detection only matched narrow K8s manifest patterns (&lt;code class=&quot;language-text&quot;&gt;apiVersion&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;kind&lt;/code&gt;), missing ArgoCD patterns like &lt;code class=&quot;language-text&quot;&gt;retry:&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;syncOptions:&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;finalizers:&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The all-tools-failed case where the model fabricated data presented in markdown tables rather than code blocks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Developer&lt;/strong&gt; implemented three targeted fixes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Added &lt;code class=&quot;language-text&quot;&gt;isAdvisoryAgent&lt;/code&gt; check in &lt;code class=&quot;language-text&quot;&gt;response.js&lt;/code&gt; to exempt IaC and Advisor agents&lt;/li&gt;
&lt;li&gt;Added &lt;code class=&quot;language-text&quot;&gt;iacSuggestionPatterns&lt;/code&gt; array in &lt;code class=&quot;language-text&quot;&gt;guard.js&lt;/code&gt; with 14 ArgoCD/Helm-specific patterns&lt;/li&gt;
&lt;li&gt;Added &lt;code class=&quot;language-text&quot;&gt;toolCallsAttempted&lt;/code&gt; counter in &lt;code class=&quot;language-text&quot;&gt;ai-chat.js&lt;/code&gt; to detect when all tool calls failed but the response contains structured data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;QA&lt;/strong&gt; ran the full 81-test e2e suite. Result: 81 passed, 3 flaky (API rate limits), 1 failed (pre-existing wrangler network issue). No regressions from the changes.&lt;/p&gt;
&lt;p&gt;Three files changed, 51 lines added, 7 removed. The entire flow from session export to validated fix happened in a single Claude Code conversation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;agents-building-agents&quot;&gt;Agents Building Agents&lt;/h2&gt;
&lt;p&gt;The most interesting aspect of this setup is that it&apos;s agents all the way down. The AI assistant running at &lt;code class=&quot;language-text&quot;&gt;/tools/ai&lt;/code&gt; uses six specialized agents (Triage, Operator, Debugger, IaC, Advisor, Network) to handle user requests. The development tool that builds and maintains that assistant uses six different specialized agents (Architect, Developer, QA, Debugger, Reviewer, Product) coordinated through a Claude Code skill.&lt;/p&gt;
&lt;p&gt;Both systems use the same core patterns: intent classification to route to the right specialist, structured handoffs to preserve context, capability levels to control what each agent can do, and quality gates to catch issues before they ship.&lt;/p&gt;
&lt;p&gt;The difference is scope. The runtime agents work with Kubernetes clusters and infrastructure. The development agents work with the codebase that defines those runtime agents. But the architecture is the same.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;setting-it-up&quot;&gt;Setting It Up&lt;/h2&gt;
&lt;p&gt;The plugin requires two files in your repo:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plugin registration&lt;/strong&gt; at &lt;code class=&quot;language-text&quot;&gt;.claude/plugins/ai-dev-studio/.claude-plugin/plugin.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai-dev-studio&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AI Development Studio for the AI DevOps assistant&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Skill definition&lt;/strong&gt; at &lt;code class=&quot;language-text&quot;&gt;.claude/commands/ai-dev-studio.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ai&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;studio
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Multi&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent AI Development Studio&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; AI Development Studio&lt;/span&gt;

You are the AI Development Studio...
[600 lines of agent specs, patterns, and workflows]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then in Claude Code, type &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt; followed by your request. The skill activates and Claude Code becomes the development studio.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building a development tool for an AI system using the same multi-agent patterns as the AI system itself felt like the natural evolution of this project. The AI assistant at &lt;code class=&quot;language-text&quot;&gt;/tools/ai&lt;/code&gt; manages Kubernetes infrastructure through specialized agents. The Claude Code plugin at &lt;code class=&quot;language-text&quot;&gt;/ai-dev-studio&lt;/code&gt; manages the assistant&apos;s codebase through specialized agents. Same architecture, different domain.&lt;/p&gt;
&lt;p&gt;The practical value is real. Exporting a broken session from the assistant, pasting it into the plugin, and getting a traced root cause with a validated fix across multiple files in a single conversation is a workflow I now use regularly. The plugin knows the codebase patterns deeply enough that the Developer agent&apos;s output matches the existing code style without manual cleanup, and the QA agent runs the actual e2e suite to catch regressions before they ship.&lt;/p&gt;
&lt;p&gt;The 600-line skill definition is just markdown describing how the codebase works and what quality standards to follow. The leverage you get from having an AI that understands your specific architecture, conventions, and testing patterns, compounds with every bug fix and feature you build through it.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Platform AI Part 3: Building a Multi-Agent System for DevOps]]></title><description><![CDATA[How I turned a single chatbot into a team of specialized AI agents that route requests, hand off between each other, and investigate Kubernetes issues through structured workflows.]]></description><link>https://chrishouse.io/platform-ai-multi-agent/</link><guid isPermaLink="false">https://chrishouse.io/platform-ai-multi-agent/</guid><pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This is Part 3 of the Platform AI series. &lt;a href=&quot;/blog/building-ai-chatbot-claude-cloudflare&quot;&gt;Part 1&lt;/a&gt; covers building the basic chatbot. &lt;a href=&quot;/blog/platform-ai-assistant&quot;&gt;Part 2&lt;/a&gt; adds agentic tool use for AKS management.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The single-agent approach from Part 2 worked fine for straightforward requests. Ask it to check cluster status, it calls the right tool. Ask it to stop a cluster, it confirms and executes. But as I started using it for real troubleshooting sessions, the cracks showed up fast.&lt;/p&gt;
&lt;p&gt;A question like &quot;why is my pod crashing?&quot; requires a different mindset than &quot;start the hub cluster.&quot; The first needs investigation, hypothesis forming, log analysis. The second needs careful execution with pre-flight checks and confirmation. Cramming both behaviors into a single system prompt creates a confused agent that either over-investigates simple operations or rushes through debugging without proper analysis.&lt;/p&gt;
&lt;p&gt;The solution was obvious once I saw the pattern. Instead of one agent trying to be everything, build a team of specialists and let them collaborate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href=&quot;https://chrishouse.io/tools/ai&quot;&gt;https://chrishouse.io/tools/ai&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-multi-agent-architecture&quot;&gt;The Multi-Agent Architecture&lt;/h2&gt;
&lt;p&gt;The new system routes requests to one of five specialized agents, each optimized for a specific type of work.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────┐     ┌─────────────────────┐     ┌──────────────────┐
│   User Message  │────&gt;│  Agent Orchestrator │────&gt;│  Selected Agent  │
└─────────────────┘     └─────────────────────┘     └──────────────────┘
                               │                           │
                               │ Intent Classification     │ Specialized
                               │ Confidence Scoring        │ System Prompt
                               │ Handoff Management        │ Filtered Tools
                               v                           v
                        ┌─────────────────────────────────────────────┐
                        │              Agent Types                     │
                        ├─────────────────────────────────────────────┤
                        │  Triage    - Status checks, overview        │
                        │  Debugger  - Investigation, root cause      │
                        │  Operator  - Safe mutations, verification   │
                        │  Advisor   - Best practices, recommendations│
                        │  IaC       - Terraform, Crossplane, GitOps  │
                        └─────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each agent has its own system prompt, a filtered set of tools, and rules for when to hand off to another specialist. The orchestrator sits in front, classifying intent and routing requests to the right agent.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-five-agents&quot;&gt;The Five Agents&lt;/h2&gt;
&lt;h3 id=&quot;triage-agent&quot;&gt;Triage Agent&lt;/h3&gt;
&lt;p&gt;Triage handles quick status checks and overview requests. When someone asks &quot;what&apos;s running?&quot; or &quot;show me the pods,&quot; they probably want a fast answer, not a deep investigation. Triage gathers the requested data and presents it clearly.&lt;/p&gt;
&lt;p&gt;Triage has read-only tools like &lt;code class=&quot;language-text&quot;&gt;get_cluster_status&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;get_pods&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;get_deployments&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;get_events&lt;/code&gt;. It cannot execute mutations. If someone asks Triage to restart a deployment, it recognizes that requires the Operator agent and initiates a handoff.&lt;/p&gt;
&lt;h3 id=&quot;debugger-agent&quot;&gt;Debugger Agent&lt;/h3&gt;
&lt;p&gt;Debugger is the investigator. When something is broken and you need to understand why, Debugger formulates hypotheses and tests them systematically. It looks at pod descriptions, pulls logs from current and previous containers, checks events, and correlates findings.&lt;/p&gt;
&lt;p&gt;The Debugger system prompt explicitly instructs it to think in terms of root cause analysis. It gathers context first, forms a hypothesis about what might be wrong, tests that hypothesis with targeted tool calls, and presents findings with confidence levels.&lt;/p&gt;
&lt;h3 id=&quot;operator-agent&quot;&gt;Operator Agent&lt;/h3&gt;
&lt;p&gt;Operator handles mutations. Starting clusters, stopping clusters, deleting pods, restarting deployments. These actions have real consequences, so Operator is built around safety. Every mutation requires user confirmation. Operator runs pre-flight checks before actions and verification checks after.&lt;/p&gt;
&lt;p&gt;The confirmation flow is baked into the agent itself. When Operator decides to call &lt;code class=&quot;language-text&quot;&gt;stop_cluster&lt;/code&gt;, it first verifies the cluster is actually running, warns about impacts like &quot;12 pods will be terminated,&quot; and waits for explicit user approval before executing.&lt;/p&gt;
&lt;h3 id=&quot;advisor-agent&quot;&gt;Advisor Agent&lt;/h3&gt;
&lt;p&gt;Advisor provides recommendations and best practices. When someone asks &quot;should I use a DaemonSet or Deployment for this?&quot; or &quot;what&apos;s the best way to handle secrets?&quot;, Advisor draws on Kubernetes patterns and platform engineering principles to give guidance.&lt;/p&gt;
&lt;p&gt;Advisor has no mutation tools at all. It can query current state for context but cannot make changes. Its role is purely consultative.&lt;/p&gt;
&lt;h3 id=&quot;iac-agent&quot;&gt;IaC Agent&lt;/h3&gt;
&lt;p&gt;IaC handles Infrastructure as Code questions. Terraform modules, Crossplane compositions, Helm charts, GitOps workflows. It knows how to read HCL syntax, understands Crossplane XRDs, and can help with ArgoCD application definitions.&lt;/p&gt;
&lt;p&gt;IaC also integrates with a RAG system that indexes the actual repository. When someone asks about a specific Terraform module, IaC can search the codebase and reference real files rather than hallucinating generic examples.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;agent-selection&quot;&gt;Agent Selection&lt;/h2&gt;
&lt;p&gt;The orchestrator classifies each incoming message and selects the most appropriate agent. This happens through pattern matching and confidence scoring.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Intent patterns for routing&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;INTENT_PATTERNS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;why.*(fail|crash|error|not working|stuck)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;troubleshoot&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;investigate&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;what.*(wrong|issue|problem)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;crashloop&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;diagnose&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;root cause&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;analyze.*(logs?|error|issue)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token literal-property property&quot;&gt;operate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\b(start|stop|restart|scale|delete|rollout)\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\b(deploy|upgrade|rollback)\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;apply.*changes?&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token literal-property property&quot;&gt;advise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;best practice&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;recommend&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;should I&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;how should&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;what.*(approach|strategy)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;optimize&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token literal-property property&quot;&gt;triage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;overview&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;health&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;what.*(running|deployed|happening)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;show.*all&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;get.*pods?&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each pattern match adds to the score for that intent. The highest scoring intent determines which agent handles the request.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;classifyIntent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scores &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;intent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; patterns&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;INTENT_PATTERNS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    scores&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;intent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pattern &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; patterns&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pattern&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        scores&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;intent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Find highest scoring intent&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; maxIntent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;triage&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Default to triage&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; maxScore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;intent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scores&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; maxScore&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      maxScore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      maxIntent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; intent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Map intent to agent&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; agentMap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;debugger&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;operate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;operator&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;advise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;advisor&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;triage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;triage&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; confidence &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; maxScore &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxScore &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; maxIntent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    confidence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;suggestedAgent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; agentMap&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;maxIntent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    scores
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A message like &quot;why is my pod crashing and restarting?&quot; hits multiple debug patterns. &quot;Crashing&quot; matches the crash pattern, &quot;why&quot; matches the why pattern, and the combination creates high confidence for the Debugger agent.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-agent-orchestrator&quot;&gt;The Agent Orchestrator&lt;/h2&gt;
&lt;p&gt;The orchestrator is the traffic controller. It maintains state about which agent is currently active, handles agent selection, and manages handoffs between agents.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AgentOrchestrator&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tools&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tools &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tools&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Initialize all agents&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; AgentClass&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AGENT_TYPES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AgentClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tools&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;currentAgent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoffHistory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onAgentSwitch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onAgentSwitch &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;selectAgent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scores &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; assessment &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;confidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; assessment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;confidence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; assessment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;canHandle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; assessment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Sort by confidence&lt;/span&gt;
    scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;confidence &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;confidence&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; best &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; scores&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// If coming from handoff, respect the suggested agent&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;targetAgent &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;targetAgent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;targetAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;confidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Handoff from &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromAgent&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;alternatives&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agent &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;targetAgent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; best&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;confidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; best&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;confidence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; best&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;alternatives&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each agent implements a &lt;code class=&quot;language-text&quot;&gt;canHandle&lt;/code&gt; method that returns a confidence score. The Debugger agent, for example, looks for investigation-related keywords and returns high confidence when it sees patterns indicating a troubleshooting session.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// From debugger-agent.js&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; score &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pattern &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEBUGGER_PATTERNS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pattern&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.25&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Coming from handoff with issue to investigate&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;findings&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Explicit debug keywords&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\b(debug|investigate|root cause|diagnose)\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; confidence &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; confidence &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    confidence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; confidence &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Request indicates investigation needed&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;May need debugging&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;agent-handoffs&quot;&gt;Agent Handoffs&lt;/h2&gt;
&lt;p&gt;Sometimes an agent realizes mid-conversation that another specialist would be better suited. The Triage agent might discover a crashing pod while checking status and hand off to Debugger. The Debugger might identify a fix and hand off to Operator to execute it.&lt;/p&gt;
&lt;p&gt;Handoffs preserve context. When Debugger hands off to Operator, it passes along what it discovered, what tools it already called, and what action it recommends. The receiving agent gets this context injected into its system prompt so it can continue intelligently rather than starting from scratch.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;handleHandoff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fromAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reason&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sourceAgent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;fromAgent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; targetAgent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agents&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;toAgent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Build handoff context&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handoffContext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    fromAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    toAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    reason&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;sourceAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buildHandoffContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;workDone &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;findings &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;toolResults &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Record handoff&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoffHistory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handoffContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Switch to new agent&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;currentAgent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; toAgent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onAgentSwitch&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onAgentSwitch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; toAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;confidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Handoff: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;reason&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;handoff&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; fromAgent
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; handoffContext&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each agent has rules about when to suggest a handoff. The Operator agent, for example, hands off to Debugger if an action fails and needs investigation.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// From operator-agent.js&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;suggestHandoff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toolResults &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// If action failed, suggest debugger&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; failedActions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; toolResults&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;error &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;failedActions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;shouldHandoff&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;targetAgent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;debugger&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Action failed: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;failedActions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;. Debugger can investigate.&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; failedActions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// If user asks for explanation after action&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;why|should|best\s*practice|recommend&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;shouldHandoff&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;targetAgent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;advisor&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;User asking for recommendations/best practices&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;completedActions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; toolResults&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;investigation-workflows&quot;&gt;Investigation Workflows&lt;/h2&gt;
&lt;p&gt;The Debugger agent is where multi-agent really shines. A simple &quot;why is my pod crashing?&quot; triggers a structured investigation.&lt;/p&gt;
&lt;p&gt;The agent first gathers context. It calls &lt;code class=&quot;language-text&quot;&gt;describe_pod&lt;/code&gt; to get the pod specification, conditions, and container statuses. It pulls current logs with &lt;code class=&quot;language-text&quot;&gt;get_pod_logs&lt;/code&gt; and previous container logs to catch errors that happened before the last restart. It checks recent events for warnings like &lt;code class=&quot;language-text&quot;&gt;OOMKilled&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;ImagePullBackOff&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All of this happens in a single reasoning loop. The agent decides which tools to call, executes them, analyzes the results, and either continues investigating or presents its findings.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Debugger agent system prompt (excerpt)&lt;/span&gt;
&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;You are the Debugger Agent for Platform AI. Your role is to:

1. **Gather Context**: Get pod status, logs, events, resource usage
2. **Form Hypotheses**: Based on symptoms, identify possible causes
3. **Test Hypotheses**: Use targeted queries to confirm or rule out causes
4. **Present Findings**: Explain root cause with evidence

## Investigation Process

When investigating an issue:
1. Start with describe_pod to understand current state
2. Pull logs (current + previous) for error messages
3. Check events for warnings (OOMKilled, ImagePullBackOff, etc.)
4. Correlate findings to identify root cause
5. Present diagnosis with confidence level

## When to Suggest Handoff
- **→ Operator**: If you&apos;ve identified a fix (restart, scale, delete)
- **→ Advisor**: If user asks for best practices to prevent recurrence
- **→ Triage**: If issue resolved and user wants status overview&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The result is an agent that thinks like a platform engineer. It does not just dump logs and hope the user figures it out. It correlates evidence, identifies patterns, and explains what went wrong and why.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-base-agent-pattern&quot;&gt;The Base Agent Pattern&lt;/h2&gt;
&lt;p&gt;All five agents extend a common base class that provides shared functionality. This keeps the architecture consistent and makes adding new agents straightforward.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BaseAgent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; BaseAgent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;BaseAgent is abstract and cannot be instantiated directly&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;BaseAgent&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;capability &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;capability &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; AgentCapability&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;READ_ONLY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;priority &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;priority &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; AgentPriority&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MEDIUM&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tools &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tools &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxIterations &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxIterations &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;icon &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;icon &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;🤖&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#6366f1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Must be implemented by subclasses&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;buildSystemPrompt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;buildSystemPrompt must be implemented by subclass&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;canHandle must be implemented by subclass&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Optional overrides&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;suggestHandoff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toolResults &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;preprocessMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;postprocessResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;response&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toolResults &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The capability system controls what agents can do. READ_ONLY agents like Triage and Advisor cannot execute mutations. EXECUTE agents like Operator require confirmation for destructive actions. This is enforced at the tool level, so even if a prompt injection tried to get Advisor to delete a pod, the tool simply would not be available.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; AgentCapability &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;READ_ONLY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;read_only&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;// Can only query/read&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;SUGGEST&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;suggest&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;// Can suggest actions&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;EXECUTE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;execute&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;// Can execute with confirmation&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;AUTONOMOUS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;autonomous&apos;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;// Can execute without confirmation (rare)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;confirmation-flow-for-mutations&quot;&gt;Confirmation Flow for Mutations&lt;/h2&gt;
&lt;p&gt;The Operator agent has a specific confirmation flow for dangerous operations. Every mutation goes through the same pattern: verify current state, present impact, wait for explicit confirmation, execute, verify result.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Actions that require confirmation&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIRMATION_REQUIRED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;start_cluster&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;high&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Starting a cluster will incur compute costs&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;stop_cluster&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;high&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Stopping will make the cluster unavailable&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;delete_pod&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;medium&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Pod will be recreated by its controller&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;restart_deployment&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;medium&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Will trigger rolling restart of all pods&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;scale_nodepool&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;high&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Will change the number of VMs (affects cost)&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Pre-flight checks for each action&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PREFLIGHT_CHECKS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;start_cluster&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;get_cluster_status&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;stop_cluster&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;get_cluster_status&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;get_pods&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;delete_pod&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;describe_pod&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;restart_deployment&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;get_deployments&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;get_pods&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;scale_nodepool&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;list_nodepools&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;get_pods&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When Operator decides to stop a cluster, it first calls &lt;code class=&quot;language-text&quot;&gt;get_cluster_status&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;get_pods&lt;/code&gt; as pre-flight checks. If the cluster is already stopped, it reports that and does not ask for confirmation. If pods are running, it warns the user how many will be terminated.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getConfirmationDetails&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;toolName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; args &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIRMATION_REQUIRED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;toolName&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;toolName&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toolName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;start_cluster&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Start cluster &quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cluster_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;stop_cluster&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Stop cluster &quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cluster_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;delete_pod&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Delete pod &quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pod_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; in &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;namespace&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;restart_deployment&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Restart deployment &quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;deployment_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; in &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;namespace&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; toolName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;level&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;warning&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    args
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-time-feedback-with-sse&quot;&gt;Real-Time Feedback with SSE&lt;/h2&gt;
&lt;p&gt;The frontend shows which agent is active and when switches happen. This is handled through Server-Sent Events that the orchestrator emits on state changes.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createOrchestratorWithSSE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tools&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; emitEvent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AgentOrchestrator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tools&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;options&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;onAgentSwitch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;selection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;emitEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;agent_switch&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; selection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; selection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;confidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; selection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;confidence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; selection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;from&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;handoff&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; selection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handoff &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The React frontend listens for these events and updates the UI. Users see a small indicator showing which agent is handling their request. When a handoff happens, they see it transition smoothly from Debugger to Operator, for example.&lt;/p&gt;
&lt;p&gt;This transparency builds trust. Users understand that when they ask an investigation question, they get the investigation specialist. When they ask to execute something, they get the operations specialist.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-layers&quot;&gt;Security Layers&lt;/h2&gt;
&lt;p&gt;Before any message reaches an agent, it passes through security filters. The system blocks requests for secrets, credentials, and sensitive data. It also catches off-topic requests before they waste model tokens.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BANNED_PATTERNS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\bsecret[s]?\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\bpassword[s]?\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\bcredential[s]?\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\bapi[_-]?key[s]?\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\bkubeconfig\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;kubectl\s+get\s+secret&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;kubectl\s+describe\s+secret&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;containsSensitiveRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BANNED_PATTERNS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pattern&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; pattern&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Off-topic detection prevents people from using the assistant for general chat, math homework, or prompt injection attempts.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;OFF_TOPIC_PATTERNS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Math&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;\d&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;\&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;\&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;\&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;\&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;x×÷]\s*\d+&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;calculate|compute|solve.*\d&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Jailbreak attempts&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;ignore\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;your&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;previous&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;all&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;instructions&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;rules&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;pretend\s*(you(&apos;re)?|to\s*be)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;you\s*are\s*now\s*(a|an|no\s*longer)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// General knowledge&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;what\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;is&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;are&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;the\s&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sun&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;moon&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;stars&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;planets&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;write\s*(me\s*)?(a\s*)?(poem|story|essay)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;OFF_TOPIC_REDIRECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;I&apos;m Platform AI - I only help with Kubernetes and infrastructure. Try asking about cluster status, deployments, pods, or Azure resources!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These filters run before the orchestrator even sees the message. A request for secrets gets rejected immediately with a clear explanation of the security policy. An off-topic request gets a polite redirect without consuming any Claude API tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;technical-decisions&quot;&gt;Technical Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why five agents instead of three or ten?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Five covers the major interaction patterns I observed in my own usage. Status checks (Triage), troubleshooting (Debugger), operations (Operator), guidance (Advisor), and infrastructure code (IaC). More agents would mean more routing complexity without clear benefit. Fewer agents would mean cramming multiple personas into single prompts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why pattern-based routing instead of LLM classification?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Speed and cost. Pattern matching happens in microseconds without an API call. LLM classification would add latency and token cost to every request. The pattern approach handles 90% of cases correctly, and for edge cases, each agent&apos;s &lt;code class=&quot;language-text&quot;&gt;canHandle&lt;/code&gt; method provides a secondary check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why hand off context explicitly instead of sharing conversation history?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Token efficiency. A full conversation history grows quickly and most of it is not relevant to the receiving agent. By passing structured handoff context, the receiving agent gets what it needs to continue without paying for irrelevant earlier messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why capability levels on agents?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Defense in depth. Even if prompt injection somehow bypassed the security filters and convinced an agent to try deleting a pod, READ_ONLY agents simply do not have the tool available. The capability level is enforced at tool binding time, not at prompt time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&apos;s Next&lt;/h2&gt;
&lt;p&gt;The handoff system could become smarter with learned routing. Instead of static patterns, the system could learn from successful interactions which agent combinations work best for which types of requests.&lt;/p&gt;
&lt;p&gt;And there is still the state machine and meta-tool system to cover, but that is another article.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Going from a single chatbot to a multi-agent system changed how I think about AI assistants. The single-agent approach forces you to write one prompt that handles everything. That works for simple cases but creates confused behavior for complex workflows.&lt;/p&gt;
&lt;p&gt;The multi-agent approach lets each specialist excel at its job. Triage is fast and focused. Debugger is methodical and thorough. Operator is safe and verified. They hand off to each other when needed, preserving context and building on each other&apos;s work.&lt;/p&gt;
&lt;p&gt;The implementation is not complicated. Each agent is maybe 200-300 lines of code. The orchestrator is another 300. The real work is in thinking through the interaction patterns and defining clear boundaries between agents.&lt;/p&gt;
&lt;p&gt;The assistant lives at &lt;code class=&quot;language-text&quot;&gt;/tools/ai&lt;/code&gt; on this blog if you want to see it in action. Ask it to check cluster status and notice the Triage agent. Ask why something is failing and watch the Debugger take over. Ask to restart a deployment and see the Operator&apos;s confirmation flow. The agents are visible in the UI, so you can follow along as they work.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Platform AI Part 2: Adding Agentic Tool Use for AKS Management]]></title><description><![CDATA[Building an AI assistant with Claude that can actually manage Kubernetes clusters - checking status, starting and stopping AKS, and handling Crossplane webhooks automatically.]]></description><link>https://chrishouse.io/platform-ai-assistant/</link><guid isPermaLink="false">https://chrishouse.io/platform-ai-assistant/</guid><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This is Part 2 of the Platform AI series. &lt;a href=&quot;/blog/building-ai-chatbot-claude-cloudflare&quot;&gt;Part 1&lt;/a&gt; covers building the basic chatbot with Claude API and Cloudflare.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most AI assistants answer questions. This one executes actions. Platform AI is an agentic assistant built into my blog that can query live cluster status, start and stop AKS clusters, and handle infrastructure operations, all through natural conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href=&quot;https://chrishouse.io/tools/ai&quot;&gt;https://chrishouse.io/tools/ai&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h2&gt;
&lt;p&gt;Platform AI is a specialized DevOps assistant that goes beyond answering questions. It can query real-time power state, node count, and Kubernetes version across all my AKS clusters. When I need to bring up a stopped cluster, I just ask it to start one and it walks me through a confirmation flow before executing. The same goes for stopping clusters, which includes automatic Crossplane webhook cleanup so the cluster doesn&apos;t get stuck on restart.&lt;/p&gt;
&lt;p&gt;Beyond cluster management, it can list pods across namespaces with their status and restart counts, fetch logs from any container, show deployment replica health, and pull recent Kubernetes events for troubleshooting. All responses are tailored to my actual infrastructure since it has context about my clusters, namespaces, and ArgoCD apps. I&apos;ve also scoped it to only answer questions about Kubernetes, Docker, Terraform, Helm, ArgoCD, Istio, and other IaC topics so it stays focused.&lt;/p&gt;
&lt;p&gt;The assistant uses Claude&apos;s tool use capability to execute real actions against Azure, not just generate text about what you &lt;em&gt;could&lt;/em&gt; do.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────┐     ┌─────────────────────┐     ┌──────────────────┐
│   React Chat    │────&gt;│  Cloudflare Pages   │────&gt;│   Claude API     │
│   Component     │     │  Function           │     │   (Haiku)        │
└─────────────────┘     └─────────────────────┘     └──────────────────┘
                               │                           │
                               │ Tool Calls                │
                               v                           │
                       ┌─────────────────────┐            │
                       │   Azure Function    │&amp;lt;───────────┘
                       │   (Managed ID)      │
                       └─────────────────────┘
                               │
                               v
                       ┌─────────────────────┐
                       │   AKS Clusters      │
                       │   (Hub + Spokes)    │
                       └─────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;components&quot;&gt;Components&lt;/h3&gt;
&lt;p&gt;The frontend is a React component with Firebase handling chat UI, message history, and session persistence. Requests go through a Cloudflare Pages Function that handles PAT authentication, routes tool calls, and formats responses. I&apos;m using Claude 3 Haiku as the AI engine since it&apos;s fast and handles tool use decisions well. The backend is an Azure Function with Managed Identity so I don&apos;t have to store any credentials for cluster operations. Firebase Realtime DB stores context caching, usage stats, and chat history.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;langchain-integration&quot;&gt;LangChain Integration&lt;/h2&gt;
&lt;p&gt;I recently refactored the Cloudflare function to use LangChain.js instead of calling the Claude API directly. This cleaned up the code significantly and made tool definitions more structured.&lt;/p&gt;
&lt;h3 id=&quot;why-langchain&quot;&gt;Why LangChain?&lt;/h3&gt;
&lt;p&gt;The raw Claude API works fine, but LangChain gives me a few things I wanted. Tool definitions use Zod schemas for input validation, which catches bad inputs before they hit my Azure Function. The message handling is cleaner with typed classes like &lt;code class=&quot;language-text&quot;&gt;HumanMessage&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;AIMessage&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;ToolMessage&lt;/code&gt;. And the agentic loop for tool execution is more maintainable.&lt;/p&gt;
&lt;h3 id=&quot;dependencies&quot;&gt;Dependencies&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;@langchain/anthropic&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^0.3.18&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;@langchain/core&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^0.3.40&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;zod&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^3.24.2&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;tool-definition-with-zod&quot;&gt;Tool Definition with Zod&lt;/h3&gt;
&lt;p&gt;Each tool is now a &lt;code class=&quot;language-text&quot;&gt;DynamicStructuredTool&lt;/code&gt; with a Zod schema:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DynamicStructuredTool &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@langchain/core/tools&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;zod&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getClusterStatusTool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DynamicStructuredTool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;get_cluster_status&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Get the current status and power state of AKS clusters.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;cluster_name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Specific cluster name to check. If omitted, returns all clusters.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cluster_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;status&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Filter by cluster_name if provided...&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Zod schema validates input types and provides descriptions that Claude uses to understand what each parameter does. If someone tries to pass an invalid type, Zod catches it before the function runs.&lt;/p&gt;
&lt;h3 id=&quot;message-handling&quot;&gt;Message Handling&lt;/h3&gt;
&lt;p&gt;LangChain provides typed message classes that make the conversation flow clearer:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; HumanMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; AIMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; SystemMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ToolMessage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@langchain/core/messages&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; messages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SystemMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;systemPrompt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;history&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    msg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;user&apos;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HumanMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;msg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AIMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;msg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HumanMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; model&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;invoke&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When Claude calls a tool, it returns a response with tool call blocks. I execute the tool, then add a &lt;code class=&quot;language-text&quot;&gt;ToolMessage&lt;/code&gt; with the result:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tool_calls&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; toolCall &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tool_calls&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tools&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AIMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;tool_calls&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;toolCall&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ToolMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;tool_call_id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Call model again to synthesize final response&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; model&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;invoke&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;benefits-ive-noticed&quot;&gt;Benefits I&apos;ve Noticed&lt;/h3&gt;
&lt;p&gt;The code is more readable now that tool definitions are self-contained objects with their schema and function together. Type safety from Zod has caught a few issues during development. And the message handling is less error-prone since I&apos;m not manually constructing JSON objects.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;tool-definitions&quot;&gt;Tool Definitions&lt;/h2&gt;
&lt;p&gt;The assistant has seven tools available when infrastructure context is enabled.&lt;/p&gt;
&lt;h3 id=&quot;cluster-management&quot;&gt;Cluster Management&lt;/h3&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;get_cluster_status&lt;/code&gt; tool returns power state, Kubernetes version, and node count for all clusters. The &lt;code class=&quot;language-text&quot;&gt;start_cluster&lt;/code&gt; tool starts a stopped AKS cluster but requires confirmation first. And &lt;code class=&quot;language-text&quot;&gt;stop_cluster&lt;/code&gt; stops a running cluster with automatic Crossplane webhook cleanup, also requiring confirmation.&lt;/p&gt;
&lt;h3 id=&quot;kubernetes-introspection&quot;&gt;Kubernetes Introspection&lt;/h3&gt;
&lt;p&gt;For looking inside clusters, &lt;code class=&quot;language-text&quot;&gt;get_pods&lt;/code&gt; lists pods with their namespace, status, ready count, and restart count. The &lt;code class=&quot;language-text&quot;&gt;get_pod_logs&lt;/code&gt; tool fetches container logs with optional tail line limits. &lt;code class=&quot;language-text&quot;&gt;get_deployments&lt;/code&gt; shows deployment status and replica health. And &lt;code class=&quot;language-text&quot;&gt;get_events&lt;/code&gt; pulls recent cluster events which is useful for debugging.&lt;/p&gt;
&lt;h3 id=&quot;tool-execution-loop&quot;&gt;Tool Execution Loop&lt;/h3&gt;
&lt;p&gt;When Claude decides to use a tool, the response includes tool call blocks. The Cloudflare function executes each tool, collects the results, and sends them back to Claude for a final response:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Claude returns tool_calls when it wants to call tools&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tool_calls&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; toolCall &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tool_calls&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;executeToolCall&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    toolResults&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ToolMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;tool_call_id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; toolCall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Send results back to Claude for final response synthesis&lt;/span&gt;
  messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;toolResults&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; model&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;invoke&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;example-get_cluster_status-response&quot;&gt;Example: get_cluster_status Response&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;clusters&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;aks-mgmt-hub&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;resourceGroup&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rg-landing-zone-hub&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;powerState&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Running&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;kubernetesVersion&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.32&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;nodeCount&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;collectedAt&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-01-14T21:10:27.274Z&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;confirmation-flow&quot;&gt;Confirmation Flow&lt;/h3&gt;
&lt;p&gt;Start and stop operations require explicit user confirmation. When I ask to start a cluster, the AI returns a message asking me to confirm with buttons. Only after I click confirm does it actually execute the operation. This prevents accidental cluster operations from a misunderstood request.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog-images/platform-ai-start-cluster.png&quot; alt=&quot;Platform AI cluster start confirmation flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;Stop operations include automatic Crossplane webhook removal. This prevents a common issue where Crossplane&apos;s validating webhooks block API calls when the cluster restarts since the webhook endpoints aren&apos;t running yet.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;smart-webhook-handling&quot;&gt;Smart Webhook Handling&lt;/h2&gt;
&lt;p&gt;When stopping a cluster that runs Crossplane, the Azure Function first gets the admin kubeconfig for the target cluster. It then deletes both the &lt;code class=&quot;language-text&quot;&gt;crossplane&lt;/code&gt; ValidatingWebhookConfiguration and MutatingWebhookConfiguration before proceeding with the cluster stop.&lt;/p&gt;
&lt;p&gt;This prevents the cluster from getting stuck in a broken state on restart where the API server can&apos;t process requests because it&apos;s waiting for webhook responses from pods that aren&apos;t running yet.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-azure-function-backend&quot;&gt;The Azure Function Backend&lt;/h2&gt;
&lt;p&gt;The backend is a Node.js Azure Function that uses Managed Identity for zero-credential authentication to Azure and Kubernetes APIs.&lt;/p&gt;
&lt;h3 id=&quot;endpoints&quot;&gt;Endpoints&lt;/h3&gt;
&lt;p&gt;The function exposes several endpoints. GET &lt;code class=&quot;language-text&quot;&gt;/status&lt;/code&gt; returns just cluster power states for quick checks. GET &lt;code class=&quot;language-text&quot;&gt;/full&lt;/code&gt; returns complete context for the AI including namespaces, apps, and XRDs. GET &lt;code class=&quot;language-text&quot;&gt;/inventory&lt;/code&gt; shows namespaces, deployments, and services. GET &lt;code class=&quot;language-text&quot;&gt;/argocd&lt;/code&gt; returns ArgoCD application sync status. GET &lt;code class=&quot;language-text&quot;&gt;/crossplane&lt;/code&gt; shows XRDs, compositions, and active claims. GET &lt;code class=&quot;language-text&quot;&gt;/istio&lt;/code&gt; returns VirtualServices and Gateways.&lt;/p&gt;
&lt;p&gt;For operations, POST &lt;code class=&quot;language-text&quot;&gt;/start-cluster&lt;/code&gt; starts a stopped AKS cluster. POST &lt;code class=&quot;language-text&quot;&gt;/stop-cluster&lt;/code&gt; stops a running cluster with webhook cleanup. POST &lt;code class=&quot;language-text&quot;&gt;/get-pods&lt;/code&gt; lists pods with status and restart counts. POST &lt;code class=&quot;language-text&quot;&gt;/get-logs&lt;/code&gt; fetches pod container logs. POST &lt;code class=&quot;language-text&quot;&gt;/get-deployments&lt;/code&gt; lists deployments with replica status. And POST &lt;code class=&quot;language-text&quot;&gt;/get-events&lt;/code&gt; gets recent Kubernetes events.&lt;/p&gt;
&lt;h3 id=&quot;key-dependencies&quot;&gt;Key Dependencies&lt;/h3&gt;
&lt;p&gt;The function uses &lt;code class=&quot;language-text&quot;&gt;@azure/identity&lt;/code&gt; with DefaultAzureCredential for Managed Identity authentication, &lt;code class=&quot;language-text&quot;&gt;@azure/arm-containerservice&lt;/code&gt; for AKS control plane operations, and &lt;code class=&quot;language-text&quot;&gt;@kubernetes/client-node&lt;/code&gt; for Kubernetes API access.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;Authentication happens automatically through &lt;code class=&quot;language-text&quot;&gt;DefaultAzureCredential&lt;/code&gt; which uses the Function App&apos;s Managed Identity. For cluster discovery, it lists all AKS clusters in the subscription via Azure Resource Manager. To access Kubernetes APIs on running clusters, it fetches the admin kubeconfig and queries directly. Start and stop operations are fire-and-forget since they take 2-5 minutes to complete.&lt;/p&gt;
&lt;p&gt;The function collects data from multiple Kubernetes APIs in parallel:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;inventory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argocd&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; crossplane&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;collectInventory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; clusters&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;collectArgoCD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;collectCrossplane&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;collectIstio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;No secrets are stored in code or config. The Managed Identity has Contributor access to the AKS clusters, and the Function App&apos;s system-assigned identity is granted cluster-admin via Azure RBAC.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;live-infrastructure-context&quot;&gt;Live Infrastructure Context&lt;/h2&gt;
&lt;p&gt;The assistant doesn&apos;t just know Kubernetes, it knows &lt;em&gt;my&lt;/em&gt; Kubernetes. When infra context is enabled, every response is informed by my actual cluster names, resource groups, versions, and power states. It knows about my namespaces including system ones and platform namespaces for ArgoCD, Crossplane, and Istio. It sees ArgoCD app sync status, health, and destinations. It understands my Crossplane setup with XRDs, active claims, and provisioned resources. And it knows about Istio gateways, VirtualServices, and traffic routing.&lt;/p&gt;
&lt;p&gt;Context comes from three sources with automatic fallback. First it tries live data from the Azure Function querying clusters in real-time. If that fails, it falls back to cached data in Firebase from hourly GitHub Actions snapshots. And if everything else fails, there&apos;s a hardcoded static fallback.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;scoped-to-devops&quot;&gt;Scoped to DevOps&lt;/h2&gt;
&lt;p&gt;The assistant refuses off-topic questions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;I can only help with Kubernetes, Docker, AKS, Azure CLI, Istio, Terraform, Helm, ArgoCD, Flux, and Infrastructure as Code questions. Please ask about those topics.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This keeps responses focused and prevents the model from hallucinating outside its configured expertise.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;ui-features&quot;&gt;UI Features&lt;/h2&gt;
&lt;h3 id=&quot;formatted-tool-results&quot;&gt;Formatted Tool Results&lt;/h3&gt;
&lt;p&gt;Tool outputs render with visual indicators to make them easy to scan. Cluster status shows green or red power state indicators. Pod lists display namespace, status, ready count, and restart count. Deployments show replica health at a glance. Events display type icons for warnings versus normal events with truncated messages. Logs render in code blocks for easy reading. Start and stop operations show the cluster name, previous state, and webhook removal details. Errors surface clearly with warning indicators.&lt;/p&gt;
&lt;h3 id=&quot;context-window-management&quot;&gt;Context Window Management&lt;/h3&gt;
&lt;p&gt;The chat stores up to 40 messages but only sends the last 20 to Claude using a sliding window approach. A pie chart in the footer shows storage usage. Under 40 messages, the pie fills proportionally as you chat. At 40 messages, the pie fills solid blue and a compact button appears on hover. Clicking compact removes the oldest 20 messages while keeping the most recent 20.&lt;/p&gt;
&lt;p&gt;This means I can have long conversations without constantly managing context. The API always sees recent messages, and I only need to compact occasionally.&lt;/p&gt;
&lt;h3 id=&quot;session-persistence&quot;&gt;Session Persistence&lt;/h3&gt;
&lt;p&gt;Chats persist to Firebase, keyed by a hash of the user&apos;s PAT. I can switch between conversations, delete old ones, or continue where I left off.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-model&quot;&gt;Security Model&lt;/h2&gt;
&lt;p&gt;Access to the API requires a Personal Access Token stored client-side. Azure authentication uses Managed Identity on the Azure Function so there are no stored credentials. Destructive actions like starting or stopping clusters require explicit user confirmation through the UI. The assistant refuses non-DevOps questions to keep it scoped. And there&apos;s per-user token tracking with visible usage stats.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&apos;s Next&lt;/h2&gt;
&lt;p&gt;I&apos;m thinking about adding ArgoCD sync triggers from chat, cost queries to see how much a cluster cost this month, Crossplane claim creation for provisioning infrastructure through conversation, and maybe kubectl exec support for running commands inside pods for debugging.&lt;/p&gt;
&lt;p&gt;The foundation is set for expanding what the assistant can do while keeping the confirmation flow for anything destructive.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Platform AI Part 1: Building a Personal AI Chatbot with Claude and Cloudflare]]></title><description><![CDATA[A walkthrough of building a specialized AI assistant for Kubernetes, Docker, and cloud questions using Claude API, React, and Cloudflare Pages Functions with PAT authentication.]]></description><link>https://chrishouse.io/building-ai-chatbot-claude-cloudflare/</link><guid isPermaLink="false">https://chrishouse.io/building-ai-chatbot-claude-cloudflare/</guid><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This is Part 1 of the Platform AI series. &lt;a href=&quot;/blog/platform-ai-assistant&quot;&gt;Part 2&lt;/a&gt; adds agentic tool use for managing AKS clusters.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wanted a specialized AI assistant that could answer my DevOps and Infrastructure as Code questions without the distraction of general-purpose chatbots. The goal was simple: build a focused tool that knows Kubernetes, Docker, AKS, Azure CLI, Istio, Terraform, Helm, and related IaC tools inside and out, and politely declines everything else.&lt;/p&gt;
&lt;p&gt;What started as a weekend project turned into a surprisingly elegant solution using Claude&apos;s API, React, Cloudflare Pages Functions, and Firebase Realtime Database for persistent chat history. The result is a clean, minimal chat interface that feels like Claude in VS Code. No fluff, just helpful answers to infrastructure questions.&lt;/p&gt;
&lt;p&gt;The assistant lives at &lt;code class=&quot;language-text&quot;&gt;/tools/ai&lt;/code&gt; on this blog and includes persistent multi-chat conversations, real-time usage tracking, configurable AI parameters (temperature and max tokens), fullscreen mode for deep-dive sessions, and a visual design that stays out of your way while you work through deployment issues or troubleshoot pod crashes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/tools/ai&quot;&gt;Try it now →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h2&gt;
&lt;p&gt;The stack is straightforward. The frontend is a Gatsby static site with a React chat component. The backend is a single Cloudflare Pages Function running serverless. Claude 3 Haiku handles all the AI heavy lifting via the Anthropic API, and Firebase Realtime Database stores persistent chat history.&lt;/p&gt;
&lt;p&gt;Authentication uses simple PAT validation where the backend checks the token against an environment variable. Scope control happens through a system prompt that enforces topic restrictions.&lt;/p&gt;
&lt;p&gt;The backend acts as a thin proxy between the frontend and Claude API, validating the PAT and injecting the system prompt that restricts Claude&apos;s responses to only Kubernetes, Docker, AKS, Azure CLI, Istio, Terraform, Helm, and Infrastructure as Code topics. Firebase Realtime Database stores chat conversations and usage statistics, keyed by a SHA-256 hash of the user&apos;s PAT for privacy.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;backend-implementation&quot;&gt;Backend Implementation&lt;/h2&gt;
&lt;h3 id=&quot;cloudflare-pages-function&quot;&gt;Cloudflare Pages Function&lt;/h3&gt;
&lt;p&gt;The backend is a single file at &lt;code class=&quot;language-text&quot;&gt;functions/api/ai-chat.js&lt;/code&gt;. Cloudflare Pages automatically discovers functions in the &lt;code class=&quot;language-text&quot;&gt;/functions&lt;/code&gt; directory and maps them to routes.&lt;/p&gt;
&lt;p&gt;The function handles PAT validation against an environment variable, forwards messages to Claude API with the system prompt, extracts usage statistics from the API response, calculates cost estimates based on token usage, and returns the response with usage metadata.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentication and Parameters:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pat&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; temperature &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxTokens &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2048&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;pat &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; pat &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ASSISTANT_PAT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Invalid access token&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;corsHeaders&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;application/json&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The backend accepts configurable &lt;code class=&quot;language-text&quot;&gt;temperature&lt;/code&gt; (defaults to 0.3 for consistent technical responses) and &lt;code class=&quot;language-text&quot;&gt;maxTokens&lt;/code&gt; (defaults to 2048) from the frontend, with validation to ensure they&apos;re within safe ranges (temperature: 0-1, maxTokens: 256-4096).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope Enforcement System Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; systemPrompt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;You are a specialized DevOps and Infrastructure as Code assistant focused exclusively on Kubernetes, Docker, AKS (Azure Kubernetes Service), Azure CLI (az), Istio, Terraform, Helm, and related IaC tools.

STRICT RULES:
1. ONLY answer questions about:
   - Kubernetes (kubectl, resources, YAML, concepts, troubleshooting)
   - Docker (Dockerfile, images, containers, compose, build)
   - AKS (Azure Kubernetes Service)
   - Azure CLI (az commands for AKS, ACR, and related services)
   - Istio (service mesh, VirtualServices, DestinationRules, traffic management)
   - Terraform (HCL, providers, state management, modules, workspaces)
   - Helm (charts, values, releases, templating, repositories)
   - Infrastructure as Code tools (Ansible, Pulumi, CloudFormation, ARM templates when related to Kubernetes/Azure)
   - GitOps tools (ArgoCD, Flux when related to K8s deployments)

2. For ANY other topics, respond with:
   &quot;I can only help with Kubernetes, Docker, AKS, Azure CLI, Istio, Terraform, Helm, and Infrastructure as Code questions. Please ask about those topics.&quot;

3. When answering in-scope questions:
   - Provide practical, actionable answers
   - Include relevant command examples in code blocks
   - Be concise but thorough
   - Explain WHY when helpful (not just HOW)
   - Use proper formatting for commands, YAML, and HCL&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Claude API Call:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; anthropicResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;https://api.anthropic.com/v1/messages&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&apos;x-api-key&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&apos;anthropic-version&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2023-06-01&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;claude-3-haiku-20240307&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxTokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4096&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;temperature&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;temperature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; systemPrompt&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; messages
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Usage Tracking:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; usage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// { input_tokens, output_tokens }&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputCost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input_tokens &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputCost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;output_tokens &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; totalCost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; inputCost &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; outputCost&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;usage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;input_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input_tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;output_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;output_tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;total_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input_tokens &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;output_tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;estimated_cost&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; totalCost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;frontend-implementation&quot;&gt;Frontend Implementation&lt;/h2&gt;
&lt;h3 id=&quot;react-chat-component&quot;&gt;React Chat Component&lt;/h3&gt;
&lt;p&gt;The chat interface lives in &lt;code class=&quot;language-text&quot;&gt;src/components/tools/AiAssistant.js&lt;/code&gt; with ~680 lines of React code managing state, API calls, Firebase integration, and UI rendering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State Management:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pat&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setPat&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  localStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ai_assistant_pat&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;patHash&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setPatHash&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;chats&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setChats&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;currentChatId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setCurrentChatId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMessages&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setInput&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;loading&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setLoading&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sessionUsage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setSessionUsage&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;cost&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;totalUsage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setTotalUsage&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;cost&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;temperature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setTemperature&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;localStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ai_assistant_temperature&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;maxTokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMaxTokens&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;localStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ai_assistant_max_tokens&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2048&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The PAT, temperature, and maxTokens persist in localStorage. Chat history and usage statistics persist in Firebase Realtime Database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PAT Hashing for Privacy:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hashPat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; encoder &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TextEncoder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; encoder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;subtle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;SHA-256&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hashArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hashBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; hashArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The PAT is hashed using SHA-256 before being used as a Firebase database key. This ensures that even if the database is compromised, the actual PAT values remain private.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;API Call Flow:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;sendMessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; chatId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; currentChatId
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;chatId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    chatId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setCurrentChatId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chatId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newMessages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;user&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newMessages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setLoading&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/api/ai-chat&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;application/json&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; newMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pat&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; temperature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxTokens &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finalMessages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;newMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;assistant&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalMessages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Track usage&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newSessionUsage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sessionUsage
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      newSessionUsage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; sessionUsage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messages &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; sessionUsage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tokens &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;total_tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;cost&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; sessionUsage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cost &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;estimated_cost&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setSessionUsage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newSessionUsage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;updateStatsInFirebase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Save chat to Firebase&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; chatData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; chatId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateChatTitle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;finalMessages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; finalMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;usage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; newSessionUsage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; chats&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;chatId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;createdAt &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;updatedAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;saveChatToFirebase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chatId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; chatData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;firebase-integration--chat-history&quot;&gt;Firebase Integration &amp;#x26; Chat History&lt;/h3&gt;
&lt;p&gt;Firebase Realtime Database stores all chat conversations and usage statistics, keyed by the SHA-256 hash of your PAT. When you enter your PAT, the app loads your complete chat history from Firebase. Every message is automatically saved, so you can close the browser and come back later to continue any conversation.&lt;/p&gt;
&lt;p&gt;The database structure is simple: chats are stored under &lt;code class=&quot;language-text&quot;&gt;ai-assistant/sessions/{patHash}/chats/{chatId}&lt;/code&gt; with messages, usage stats, and timestamps. Aggregate usage statistics are stored separately under &lt;code class=&quot;language-text&quot;&gt;ai-assistant/stats/{patHash}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chat History Sidebar:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A separate &lt;code class=&quot;language-text&quot;&gt;AiChatHistory&lt;/code&gt; component displays all previous conversations in a right sidebar. It lists all chats sorted by most recently updated and shows the chat title (first 40 characters of the first user message). Dates use relative formatting like &quot;Today&quot;, &quot;Yesterday&quot;, or &quot;X days ago&quot;. The currently active chat gets highlighted, each chat has a delete button, and there&apos;s a new chat button in the sidebar header.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;configurable-ai-parameters&quot;&gt;Configurable AI Parameters&lt;/h2&gt;
&lt;p&gt;The UI includes sliders in the settings panel to adjust Claude&apos;s behavior.&lt;/p&gt;
&lt;p&gt;The temperature slider (0 to 1, step 0.1) controls response randomness and creativity. It defaults to 0.3 for consistent, focused technical answers. Lower values between 0 and 0.3 give you more deterministic responses, which works better for kubectl commands and YAML configs. Higher values between 0.7 and 1.0 are more creative and useful for brainstorming solutions.&lt;/p&gt;
&lt;p&gt;The max tokens slider (256 to 4096, step 256) controls maximum response length. The default is 2048 tokens, roughly 1500 words. Lower values give you shorter, more concise answers. Higher values let Claude write longer, more detailed explanations.&lt;/p&gt;
&lt;p&gt;The context window slider (10 to 50 messages, step 5) controls how many recent messages are sent to the API. It defaults to 20 messages. Lower values like 10 or 15 reduce context but cost less and respond faster. Higher values like 30 or 50 give more context for complex troubleshooting sessions. The full conversation history is always preserved locally and in Firebase regardless of this setting.&lt;/p&gt;
&lt;p&gt;All settings persist in localStorage and are sent with each API request. The backend validates and clamps temperature and maxTokens to safe ranges before passing them to the Claude API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UI Implementation:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;input
  type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;range&quot;&lt;/span&gt;
  min&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;
  max&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;
  step&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0.1&quot;&lt;/span&gt;
  value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;temperature&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  onChange&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTemperature&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai-chat-settings-slider&quot;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;input
  type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;range&quot;&lt;/span&gt;
  min&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;256&quot;&lt;/span&gt;
  max&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;4096&quot;&lt;/span&gt;
  step&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;256&quot;&lt;/span&gt;
  value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;maxTokens&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  onChange&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setMaxTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai-chat-settings-slider&quot;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;input
  type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;range&quot;&lt;/span&gt;
  min&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;10&quot;&lt;/span&gt;
  max&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;50&quot;&lt;/span&gt;
  step&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;5&quot;&lt;/span&gt;
  value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;contextWindow&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  onChange&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setContextWindow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai-chat-settings-slider&quot;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;sliding-window-context-management&quot;&gt;Sliding Window Context Management&lt;/h3&gt;
&lt;p&gt;The assistant implements a sliding window to prevent conversations from becoming too expensive as they grow longer. Without this, each new message would include the entire conversation history, causing token usage to compound exponentially.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Apply sliding window: only send last N messages to API&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; messagesToSend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newMessages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; contextWindow
  &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; newMessages
  &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; newMessages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;contextWindow&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/api/ai-chat&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;application/json&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; messagesToSend&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pat&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; temperature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxTokens &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The full conversation history remains stored locally and in Firebase, but only the most recent messages (configurable, default 20) are sent to the Claude API. This keeps costs constant per message since each API call uses roughly the same number of tokens regardless of conversation length. Responses come back faster because smaller context means quicker processing. It also prevents exponential growth where a 50-message conversation would send 1,275 messages total to the API (1+2+3...+50). With a 20-message window, it only sends 695 messages total. Your full conversation is never lost, it&apos;s just not all sent to the API each time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;deployment-configuration&quot;&gt;Deployment Configuration&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Environment Variables:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Local development uses &lt;code class=&quot;language-text&quot;&gt;.dev.vars&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;ANTHROPIC_API_KEY=TOP_SECRET...
ASSISTANT_PAT=YOURPAT&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Production uses Cloudflare secrets (set via dashboard or CLI).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Firebase Setup:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a Firebase project and enable Realtime Database. Head to the &lt;a href=&quot;https://console.firebase.google.com/&quot;&gt;Firebase Console&lt;/a&gt; and create a new project or use an existing one. Enable Realtime Database in the &quot;Build&quot; section and set the database rules for public read/write since we&apos;re using PAT hashing for privacy:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;rules&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;.read&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;.write&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add your Firebase config to &lt;code class=&quot;language-text&quot;&gt;src/lib/firebase.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firebaseConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;apiKey&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;YOUR_API_KEY&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;authDomain&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-project.firebaseapp.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;databaseURL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://your-project-default-rtdb.firebaseio.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;projectId&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-project&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Firebase SDK is modular and tree-shakeable, adding only ~15-20KB gzipped to the bundle size.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;wrangler.jsonc:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blog&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;compatibility_date&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2025-12-25&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;pages_build_output_dir&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./public&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Build and Deploy:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Build Gatsby site&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run build

&lt;span class=&quot;token comment&quot;&gt;# Deploy to Cloudflare Pages&lt;/span&gt;
npx wrangler pages deploy public --project-name&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;blog&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Cloudflare Pages deployment automatically serves the Gatsby static site and makes the Pages Function available at &lt;code class=&quot;language-text&quot;&gt;/api/ai-chat&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;usage-statistics&quot;&gt;Usage Statistics&lt;/h2&gt;
&lt;p&gt;The UI displays real-time usage information in multiple locations.&lt;/p&gt;
&lt;p&gt;The footer (always visible after the first message) shows current chat session stats including message count, total tokens, and cost, plus all-time cumulative stats across all chats.&lt;/p&gt;
&lt;p&gt;The settings panel displays all-time total usage statistics, the last message&apos;s token usage and cost, and an estimate of remaining messages in a $5 API balance.&lt;/p&gt;
&lt;p&gt;In fullscreen mode, session stats appear in the header for quick reference.&lt;/p&gt;
&lt;p&gt;Each chat conversation tracks its own usage in terms of messages, tokens, and cost. Total usage gets aggregated across all chats and persisted in Firebase. Token counts come from the Claude API response, so they&apos;re exact rather than estimates. Cost calculation uses Claude 3 Haiku pricing at $0.25 per million input tokens and $1.25 per million output tokens.&lt;/p&gt;
&lt;p&gt;When switching between chats, the session usage stats update to reflect that specific conversation&apos;s usage.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cost-analysis&quot;&gt;Cost Analysis&lt;/h2&gt;
&lt;p&gt;Based on initial testing, a typical question/answer exchange uses approximately 500-1500 tokens total, costing between $0.0005 and $0.002 per message. A $5 prepaid balance on the Claude API should provide several hundred interactions depending on response length.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sliding window prevents cost escalation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Without the sliding window, costs would compound exponentially as conversations grow. Message 1 uses roughly 500 tokens. Message 2 jumps to 1000 tokens because it includes the previous message. By message 10 you&apos;re at 5000 tokens, message 20 hits 10,000 tokens, and message 50 balloons to 25,000 tokens per API call.&lt;/p&gt;
&lt;p&gt;With the default 20-message sliding window, messages 1 through 20 grow naturally up to around 10,000 tokens. But message 21 and beyond stay constant at roughly 10,000 tokens per API call. This means a 50-message conversation costs roughly the same as two 25-message conversations instead of exponentially more. The sliding window is the difference between spending $0.50 on a long troubleshooting session versus $5 or more.&lt;/p&gt;
&lt;p&gt;I went with Claude 3 Haiku (&lt;code class=&quot;language-text&quot;&gt;claude-3-haiku-20240307&lt;/code&gt;) for the model. I originally planned to use Claude 3.5 Sonnet, but it&apos;s not available on the prepaid API tier. Haiku turned out to be sufficient for Kubernetes and Docker questions at a lower cost, and the response times are faster than Sonnet anyway.&lt;/p&gt;
&lt;p&gt;There&apos;s no rate limiting implemented currently. PAT access control is sufficient for personal use, though I could add rate limiting via Cloudflare Workers KV or Durable Objects if this ever needs to support more users.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;technical-decisions&quot;&gt;Technical Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why Cloudflare Pages Functions instead of direct API calls:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Going through a backend keeps the API key secure, enables PAT validation, and allows injection of the system prompt without exposing it client-side. Plus it&apos;s serverless, so there&apos;s no infrastructure to manage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why PAT instead of OAuth:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is a personal use case with no multi-user requirements. A simple PAT implementation means no additional dependencies or auth providers to deal with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Firebase Realtime Database for persistence:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The free tier is generous at 1GB storage and 10GB per month bandwidth. Firebase has real-time sync capabilities that aren&apos;t used yet but are there if needed. The key-value structure is perfect for chat storage with no server or database management required. The modular SDK only adds about 15-20KB gzipped to the bundle. I considered Cloudflare KV or Durable Objects but they&apos;re more complex for this use case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why hash PAT before using as database key:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SHA-256 is a one-way hash, so the PAT can&apos;t be recovered from the database even if it gets compromised. Each user&apos;s data is isolated by their unique hash without needing an additional authentication or encryption layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why system prompt for scope enforcement:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude 3 Haiku is excellent at following instructions. No complex filtering logic needed. It works reliably across the entire conversation context as long as you give it a specific topic list rather than general categories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Claude 3 Haiku instead of Sonnet:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Already covered this above, but the short version is Sonnet isn&apos;t available on prepaid, Haiku is sufficient for the use case, and it&apos;s faster and cheaper. Match the model capability to your task requirements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why sliding window for context management:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The sliding window prevents exponential token growth in long conversations and maintains constant cost per message after reaching the window size. I considered sending the full conversation (simple but expensive for long sessions), conversation compacting or summarization (complex and loses detail), or manual context reset (annoying user experience). The sliding window balances cost, performance, and UX without requiring manual intervention. Users can adjust the window size from 10 to 50 messages based on their needs, and the full history is still preserved for reference and chat switching.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&apos;s Next&lt;/h2&gt;
&lt;p&gt;Future improvements could include conversation search and filtering, exporting chat history to markdown or JSON, code syntax highlighting in responses, and sharing individual conversations via URL.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building this took maybe 4-5 hours spread across a weekend for the initial version, plus another few hours to add Firebase persistence and multi-chat support. The result is a specialized AI assistant that lives at &lt;code class=&quot;language-text&quot;&gt;/tools/ai&lt;/code&gt; on this blog, answers DevOps questions without distraction, remembers all your conversations, tracks its own costs, and feels like a native part of my workflow.&lt;/p&gt;
&lt;p&gt;The architecture is elegantly simple: a static React frontend, a serverless Cloudflare Function as a thin proxy, Claude&apos;s API doing the heavy lifting, and Firebase Realtime Database storing chat history. No authentication servers, no complex infrastructure, no container orchestration. Just four components working together.&lt;/p&gt;
&lt;p&gt;What surprised me most was how well the system prompt enforcement works. I expected to need keyword filtering or some kind of ML-based topic classification. Nope, just clear instructions in natural language, and Claude 3 Haiku follows them reliably. The Firebase integration was similarly straightforward. Hash the PAT for privacy, save chats on each message, load them on mount. Done.&lt;/p&gt;
&lt;p&gt;The persistent chat history turned out to be more valuable than I expected. Being able to revisit past troubleshooting sessions or reference previous kubectl commands makes the tool feel less like a chatbot and more like a personal knowledge base that happens to answer questions.&lt;/p&gt;
&lt;p&gt;If you&apos;re building something similar, here&apos;s my advice: start simple, match the model to the task, and don&apos;t over-engineer. Add features iteratively based on actual usage. The best tools are the ones that disappear, letting you focus on the actual problem you&apos;re trying to solve.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Building Golden Paths with Backstage: Part 3 - Production from Day One]]></title><description><![CDATA[Why separate promotion workflows are unnecessary: building production infrastructure into your golden path template from the start with optional toggles.]]></description><link>https://chrishouse.io/golden-path-implementation-part-3-production-template/</link><guid isPermaLink="false">https://chrishouse.io/golden-path-implementation-part-3-production-template/</guid><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In &lt;a href=&quot;/golden-path-implementation-part-1-foundation&quot;&gt;Part 1&lt;/a&gt;, we built the foundation: a shared development cluster with namespace isolation and a quick-start template that gets developers from zero to deployed in under 5 minutes. In &lt;a href=&quot;/golden-path-implementation-part-2-preview-environments&quot;&gt;Part 2&lt;/a&gt;, we added preview environments that automatically spin up for every pull request.&lt;/p&gt;
&lt;p&gt;Now it&apos;s time for production.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-wrong-approach-twice&quot;&gt;The Wrong Approach (Twice)&lt;/h2&gt;
&lt;p&gt;My first attempt at a production template was basically a copy of the quick-start template that pointed to a different cluster. Create a new repository, scaffold a new application, deploy to production. That was obviously wrong.&lt;/p&gt;
&lt;p&gt;My second attempt was a &quot;promote to production&quot; template. A developer who already had a service in dev would run a separate template that created production infrastructure for their existing repo. Better, but still wrong.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I realized: &lt;strong&gt;if you know a service will eventually need production infrastructure, why not create it from the start?&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-insight-production-is-just-configuration&quot;&gt;The Insight: Production is Just Configuration&lt;/h2&gt;
&lt;p&gt;The difference between development and production isn&apos;t the code or the repository. It&apos;s configuration:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Development&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;shared-dev-cluster&lt;/td&gt;
&lt;td&gt;aks-app-spoke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;team-dev&lt;/td&gt;
&lt;td&gt;team-prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image tag&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;v1.0.0&lt;/code&gt; (semver)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replicas&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;disabled&lt;/td&gt;
&lt;td&gt;enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;{service}.chrishouse.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;{service}-prod.chrishouse.io&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;Hub → East-West Gateway&lt;/td&gt;
&lt;td&gt;Hub → Spoke Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If all of this is just YAML configuration, why can&apos;t we generate it all at once?&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;one-template-both-environments&quot;&gt;One Template, Both Environments&lt;/h2&gt;
&lt;p&gt;Instead of two separate templates (quick-start + promote), I merged them into a single golden path template with an optional production toggle:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service Details
    &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;service_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service Name
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
      &lt;span class=&quot;token key atrule&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Team
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
        &lt;span class=&quot;token key atrule&quot;&gt;ui:field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; OwnerPicker
      &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Description
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Production Configuration
    &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;includeProduction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Include Production Infrastructure
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; boolean
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create production ArgoCD app&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; namespace&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; and ingress routes

      &lt;span class=&quot;token key atrule&quot;&gt;prodReplicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Production Replicas
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; integer
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;minimum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;

      &lt;span class=&quot;token key atrule&quot;&gt;enableHPA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Enable Horizontal Pod Autoscaling
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; boolean
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The magic is in that &lt;code class=&quot;language-text&quot;&gt;includeProduction&lt;/code&gt; checkbox. Default is true, but developers can uncheck it if they&apos;re just experimenting.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-gets-created&quot;&gt;What Gets Created&lt;/h2&gt;
&lt;p&gt;When a developer runs the template with production enabled, a single PR contains everything:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;infra/
├── quickstart-services/my-service/
│   ├── namespace-claim.yaml        # Dev namespace
│   ├── namespace-claim-prod.yaml   # Prod namespace
│   ├── argocd-application.yaml     # Dev ArgoCD app (tracks latest)
│   ├── argocd-application-prod.yaml # Prod ArgoCD app (tracks semver)
│   └── README.md
└── kubernetes/
    └── istio-hub/
        ├── virtualservice-my-service-dev.yaml   # Dev routing
        └── virtualservice-my-service-prod.yaml  # Prod routing&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Six files. One PR. Both environments ready.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The template itself has separate folders (&lt;code class=&quot;language-text&quot;&gt;gitops-manifests-dev/&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;gitops-manifests-prod/&lt;/code&gt;) as building blocks that get conditionally included, but the output is unified - everything lands in the same PR.&lt;/p&gt;
&lt;p&gt;Compare this to the two-template approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run quick-start template → PR #1&lt;/li&gt;
&lt;li&gt;Wait for approval and merge&lt;/li&gt;
&lt;li&gt;Develop for a while&lt;/li&gt;
&lt;li&gt;Run promote template → PR #2&lt;/li&gt;
&lt;li&gt;Wait for approval and merge&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now it&apos;s just:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run golden path template → PR #1&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conditional-steps-in-backstage&quot;&gt;Conditional Steps in Backstage&lt;/h2&gt;
&lt;p&gt;The template uses conditional steps to skip production resources when not needed:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# Always runs - creates dev infrastructure&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gitops&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create Dev GitOps Configuration
    &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;template
    &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./gitops&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev
      &lt;span class=&quot;token key atrule&quot;&gt;targetPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./gitops/quickstart&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;services/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;# Only runs if production is enabled&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gitops&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create Prod GitOps Configuration
    &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.includeProduction &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;template
    &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./gitops&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
      &lt;span class=&quot;token key atrule&quot;&gt;targetPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./gitops/quickstart&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;services/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;# Only runs if production is enabled&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;hub&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;vs
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create Hub Ingress Route
    &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.includeProduction &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;template
    &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./hub&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;virtualservice
      &lt;span class=&quot;token key atrule&quot;&gt;targetPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./gitops/kubernetes/istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;hub&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt; clause makes steps conditional. When &lt;code class=&quot;language-text&quot;&gt;includeProduction&lt;/code&gt; is false, the production steps are skipped entirely.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-production-argocd-application&quot;&gt;The Production ArgoCD Application&lt;/h2&gt;
&lt;p&gt;The production app uses ArgoCD Image Updater for automatic version detection:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;service&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd-image-updater.argoproj.io/image-list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;app=ghcr.io/crh225/my-service&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd-image-updater.argoproj.io/app.update-strategy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;semver&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd-image-updater.argoproj.io/app.allow-tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;regexp:^v[0-9]+\\.[0-9]+\\.[0-9]+$&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/crh225/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;service
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; helm
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; image.tag
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;v1.0.0&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Initial version&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; replicaCount
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; autoscaling.enabled
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aks&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;spoke
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; team&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This creates a trunk-based release flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Development&lt;/strong&gt;: Push to main → builds &lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt; → auto-deploys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production&lt;/strong&gt;: Create GitHub Release → builds &lt;code class=&quot;language-text&quot;&gt;v1.x.x&lt;/code&gt; → Image Updater detects and deploys&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;traffic-flow&quot;&gt;Traffic Flow&lt;/h2&gt;
&lt;p&gt;Both dev and prod environments get external URLs through the Istio mesh. The key difference is which cluster receives the traffic.&lt;/p&gt;
&lt;h3 id=&quot;development-traffic&quot;&gt;Development Traffic&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;https://my-service.chrishouse.io
         │
         ▼
┌─────────────────────────────────────────┐
│ Hub Cluster (aks-mgmt-hub)              │
│                                         │
│ main-gateway                            │
│     │                                   │
│     ▼                                   │
│ VirtualService: my-service-dev-vs       │
│     │                                   │
│     ▼                                   │
│ ServiceEntry: shared-dev.internal       │
│     → shared-dev east-west gateway IP   │
└─────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│ Shared Dev Cluster                      │
│                                         │
│ cross-network-gateway (east-west)       │
│     │                                   │
│     ▼                                   │
│ VirtualService: my-service              │
│     │                                   │
│     ▼                                   │
│ my-service.team-dev.svc.cluster.local   │
└─────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;production-traffic&quot;&gt;Production Traffic&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;https://my-service-prod.chrishouse.io
         │
         ▼
┌─────────────────────────────────────────┐
│ Hub Cluster (aks-mgmt-hub)              │
│                                         │
│ main-gateway                            │
│     │                                   │
│     ▼                                   │
│ VirtualService: my-service-prod-vs      │
│     │                                   │
│     ▼                                   │
│ ServiceEntry: spoke.internal            │
│     → app-spoke gateway IP              │
└─────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│ App Spoke Cluster (aks-app-spoke)       │
│                                         │
│ cross-network-gateway (east-west)       │
│     │                                   │
│     ▼                                   │
│ VirtualService: my-service              │
│     │                                   │
│     ▼                                   │
│ my-service.team-prod.svc.cluster.local  │
└─────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Both services are immediately accessible at their URLs after ArgoCD syncs. No port-forwarding, no VPN - just HTTPS to a real domain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;This change is more significant than it might appear:&lt;/p&gt;
&lt;h3 id=&quot;1-reduced-cognitive-load&quot;&gt;1. Reduced Cognitive Load&lt;/h3&gt;
&lt;p&gt;Developers don&apos;t need to learn two templates. There&apos;s one golden path that handles everything. The mental model is simpler: &quot;run the template, get a service.&quot;&lt;/p&gt;
&lt;h3 id=&quot;2-production-ready-from-day-one&quot;&gt;2. Production-Ready from Day One&lt;/h3&gt;
&lt;p&gt;Even if a developer doesn&apos;t release to production immediately, the infrastructure is waiting. When they&apos;re ready, it&apos;s just:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a GitHub Release with tag &lt;code class=&quot;language-text&quot;&gt;v1.0.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CI builds the image&lt;/li&gt;
&lt;li&gt;ArgoCD deploys to production&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No additional PRs, no waiting for platform team approval again.&lt;/p&gt;
&lt;h3 id=&quot;3-consistent-configuration&quot;&gt;3. Consistent Configuration&lt;/h3&gt;
&lt;p&gt;When dev and prod are created together, they&apos;re guaranteed to be consistent. Same Helm chart, same value patterns, same team ownership. No drift between what was created in dev months ago and what gets created for prod now.&lt;/p&gt;
&lt;h3 id=&quot;4-faster-time-to-production&quot;&gt;4. Faster Time to Production&lt;/h3&gt;
&lt;p&gt;The two-template approach had a hidden cost: the second template was often run weeks or months after the first. By then, developers had forgotten the exact configuration, team names had changed, and the promotion became a mini-project.&lt;/p&gt;
&lt;p&gt;With everything created upfront, the path to production is just a git tag away.&lt;/p&gt;
&lt;h3 id=&quot;5-optional-complexity&quot;&gt;5. Optional Complexity&lt;/h3&gt;
&lt;p&gt;The checkbox makes it optional. Experimenting with something that will never go to production? Uncheck the box. Building a real service? Leave it checked (the default).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-complete-golden-path&quot;&gt;The Complete Golden Path&lt;/h2&gt;
&lt;p&gt;The journey from idea to production is now:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create&lt;/td&gt;
&lt;td&gt;Run golden path template&lt;/td&gt;
&lt;td&gt;Dev + Prod infrastructure created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Develop&lt;/td&gt;
&lt;td&gt;Push to main&lt;/td&gt;
&lt;td&gt;Auto-deploys to dev cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;Open PR&lt;/td&gt;
&lt;td&gt;Preview environment spins up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release&lt;/td&gt;
&lt;td&gt;Create GitHub Release v1.0.0&lt;/td&gt;
&lt;td&gt;Production deploys automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One template. One PR. Both environments. Production when you&apos;re ready.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;developer-experience&quot;&gt;Developer Experience&lt;/h2&gt;
&lt;p&gt;From the developer&apos;s perspective:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open Backstage&lt;/strong&gt; and select &quot;Golden Path - Node.js Microservice&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enter service details&lt;/strong&gt; (name, team, description)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure production&lt;/strong&gt; (replicas, HPA) or uncheck to skip&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Click create&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The PR includes everything. After merge:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dev deploys immediately on every push to main&lt;/li&gt;
&lt;li&gt;Production waits for a GitHub Release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When ready for production:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to your repository&lt;/li&gt;
&lt;li&gt;Create a Release with tag &lt;code class=&quot;language-text&quot;&gt;v1.0.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wait 2-3 minutes&lt;/li&gt;
&lt;li&gt;Service is live at &lt;code class=&quot;language-text&quot;&gt;{service}-prod.chrishouse.io&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;series-progress&quot;&gt;Series Progress&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/golden-path-implementation-part-1-foundation&quot;&gt;Part 1: Foundation&lt;/a&gt; - Shared clusters, namespace isolation, golden path template&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/golden-path-implementation-part-2-preview-environments&quot;&gt;Part 2: Preview Environments&lt;/a&gt; - Ephemeral PR environments with Istio multi-cluster routing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: Production from Day One&lt;/strong&gt; (this post) - Optional production infrastructure in the initial template&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Merge, don&apos;t separate.&lt;/strong&gt; If dev and prod use the same code, create their infrastructure together.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make it optional.&lt;/strong&gt; A checkbox is all you need. Default to &quot;yes&quot; for real services.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reduce the number of workflows.&lt;/strong&gt; Every additional template is cognitive overhead. One template that does everything is better than specialized templates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Production-ready doesn&apos;t mean production-deployed.&lt;/strong&gt; Create the infrastructure upfront; deploy when ready via git tags.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Time to production matters.&lt;/strong&gt; The faster developers can go from idea to production, the more value they deliver. Remove every unnecessary step.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The golden path is now truly golden: one template creates everything a service needs, from development through production. The only thing left is writing the code.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;implementation-repository&quot;&gt;Implementation Repository&lt;/h2&gt;
&lt;p&gt;Full implementation: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal&quot;&gt;github.com/crh225/ARMServicePortal&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Key files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Golden path template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/backstage/templates/nodejs-quickstart/template.yaml&quot;&gt;backstage/templates/nodejs-quickstart/template.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dev manifests: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/tree/main/backstage/templates/nodejs-quickstart/gitops-manifests-dev&quot;&gt;backstage/templates/nodejs-quickstart/gitops-manifests-dev/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Prod manifests: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/tree/main/backstage/templates/nodejs-quickstart/gitops-manifests-prod&quot;&gt;backstage/templates/nodejs-quickstart/gitops-manifests-prod/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Hub VirtualService template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/tree/main/backstage/templates/nodejs-quickstart/hub-virtualservice&quot;&gt;backstage/templates/nodejs-quickstart/hub-virtualservice/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spoke VirtualService template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/tree/main/backstage/templates/nodejs-quickstart/spoke-virtualservice&quot;&gt;backstage/templates/nodejs-quickstart/spoke-virtualservice/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Adding Auth0 Authentication to Backstage: A Complete Guide]]></title><description><![CDATA[Step-by-step guide to implementing Auth0 OAuth authentication in Backstage, replacing insecure guest authentication with proper identity management for both local development and production Kubernetes deployments.]]></description><link>https://chrishouse.io/backstage-auth0-authentication/</link><guid isPermaLink="false">https://chrishouse.io/backstage-auth0-authentication/</guid><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Backstage&apos;s default guest authentication works well for development, but it&apos;s not designed for production use. The &lt;code class=&quot;language-text&quot;&gt;dangerouslyAllowOutsideDevelopment: true&lt;/code&gt; flag exists as a workaround, but as the name suggests, it&apos;s not ideal for real deployments. Auth0&apos;s free tier offers a straightforward way to add proper identity management without the complexity of enterprise SSO solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we&apos;re building:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth0 OAuth integration with Backstage&lt;/li&gt;
&lt;li&gt;User entity resolution (matching Auth0 emails to catalog users)&lt;/li&gt;
&lt;li&gt;Production-ready Kubernetes deployment with Azure Key Vault secrets&lt;/li&gt;
&lt;li&gt;Guest fallback only in development mode&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Backstage app (new backend system)&lt;/li&gt;
&lt;li&gt;Auth0 account (free tier works)&lt;/li&gt;
&lt;li&gt;For production: Kubernetes cluster with CSI Secret Store Driver&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-1-auth0-setup&quot;&gt;Part 1: Auth0 Setup&lt;/h2&gt;
&lt;h3 id=&quot;create-an-auth0-application&quot;&gt;Create an Auth0 Application&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Log into &lt;a href=&quot;https://manage.auth0.com/&quot;&gt;Auth0 Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Applications &gt; Applications &gt; Create Application&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Regular Web Application&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name it something like &quot;Backstage&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;configure-callback-urls&quot;&gt;Configure Callback URLs&lt;/h3&gt;
&lt;p&gt;In your Auth0 application settings, add:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Allowed Callback URLs:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:7007/api/auth/auth0/handler/frame
https://backstage.yourdomain.com/api/auth/auth0/handler/frame&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Allowed Logout URLs:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:3000
https://backstage.yourdomain.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Allowed Web Origins:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:3000
https://backstage.yourdomain.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;note-your-credentials&quot;&gt;Note Your Credentials&lt;/h3&gt;
&lt;p&gt;From the Auth0 application settings, grab:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain&lt;/strong&gt; (e.g., &lt;code class=&quot;language-text&quot;&gt;dev-xxxxx.us.auth0.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-2-backend-configuration&quot;&gt;Part 2: Backend Configuration&lt;/h2&gt;
&lt;h3 id=&quot;install-the-auth0-provider-module&quot;&gt;Install the Auth0 Provider Module&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; packages/backend
&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; @backstage/plugin-auth-backend-module-auth0-provider&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;register-the-module&quot;&gt;Register the Module&lt;/h3&gt;
&lt;p&gt;In &lt;code class=&quot;language-text&quot;&gt;packages/backend/src/index.ts&lt;/code&gt;, add the Auth0 module:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createBackend &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@backstage/backend-defaults&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; backend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBackend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Core plugins&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-app-backend&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-catalog-backend&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-scaffolder-backend&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Auth plugins&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-auth-backend&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Auth0 provider - production authentication&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-auth-backend-module-auth0-provider&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Guest provider - development convenience only&lt;/span&gt;
backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;@backstage/plugin-auth-backend-module-guest-provider&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... other plugins&lt;/span&gt;

backend&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;configure-app-configyaml&quot;&gt;Configure app-config.yaml&lt;/h3&gt;
&lt;p&gt;Add the Auth0 configuration:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; development
  &lt;span class=&quot;token key atrule&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_SESSION_SECRET&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;providers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Guest provider for local development only&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;guest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Auth0 OAuth provider&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;auth0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;development&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;clientId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_CLIENT_ID&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;clientSecret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_CLIENT_SECRET&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_DOMAIN&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;signIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;resolvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;# Match Auth0 email with Backstage User entity profile email&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; emailMatchingUserEntityProfileEmail
            &lt;span class=&quot;token comment&quot;&gt;# Fallback: Match email local part (before @) with User entity name&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; emailLocalPartMatchingUserEntityName&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The sign-in resolvers determine how Auth0 users get mapped to Backstage User entities. The first resolver that finds a match is used.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-3-frontend-configuration&quot;&gt;Part 3: Frontend Configuration&lt;/h2&gt;
&lt;h3 id=&quot;create-the-auth0-api-reference&quot;&gt;Create the Auth0 API Reference&lt;/h3&gt;
&lt;p&gt;Backstage doesn&apos;t export an &lt;code class=&quot;language-text&quot;&gt;auth0AuthApiRef&lt;/code&gt; from the core packages, so you&apos;ll need to create one yourself. This API reference connects the frontend to the Auth0 OAuth flow. In &lt;code class=&quot;language-text&quot;&gt;packages/app/src/apis.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  createApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  OpenIdConnectApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ProfileInfoApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  BackstageIdentityApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  SessionApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  configApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  discoveryApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  oauthRequestApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createApiFactory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@backstage/core-plugin-api&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; OAuth2 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@backstage/core-app-api&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Create Auth0 API reference (not exported from core-plugin-api)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth0AuthApiRef&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiRef&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
  OpenIdConnectApi &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; ProfileInfoApi &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; BackstageIdentityApi &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; SessionApi
&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createApiRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;internal.auth.auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apis&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AnyApiFactory&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ... existing APIs&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Auth0 OAuth API factory&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;createApiFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; auth0AuthApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    deps&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      discoveryApi&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; discoveryApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      oauthRequestApi&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; oauthRequestApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      configApi&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; configApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; discoveryApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; oauthRequestApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; configApi &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
      OAuth2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        discoveryApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        oauthRequestApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token function-variable function&quot;&gt;icon&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        environment&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; configApi&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOptionalString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;auth.environment&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        defaultScopes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;openid&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;profile&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;configure-the-signinpage&quot;&gt;Configure the SignInPage&lt;/h3&gt;
&lt;p&gt;In &lt;code class=&quot;language-text&quot;&gt;packages/app/src/App.tsx&lt;/code&gt;, configure the sign-in page to use the Auth0 provider:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SignInPage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@backstage/core-components&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth0AuthApiRef &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./apis&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; app &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createApp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ... other config&lt;/span&gt;
  components&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;SignInPage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;SignInPage
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        providers&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Sign in with Auth0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            apiRef&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; auth0AuthApiRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        title&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Your App Name&quot;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The built-in &lt;code class=&quot;language-text&quot;&gt;SignInPage&lt;/code&gt; component handles the OAuth popup flow automatically. When users click the sign-in button, a popup window opens for Auth0 authentication, then redirects back to your app once complete.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-4-user-entity-setup&quot;&gt;Part 4: User Entity Setup&lt;/h2&gt;
&lt;p&gt;Auth0 users must match Backstage User entities for sign-in to work. Create or update &lt;code class=&quot;language-text&quot;&gt;examples/org.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; User
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chris
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Chris House
    &lt;span class=&quot;token key atrule&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chris@example.com  &lt;span class=&quot;token comment&quot;&gt;# Must match Auth0 user email&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;memberOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;platform&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;team&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Group
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;team
  &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Platform engineering team
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; team
  &lt;span class=&quot;token key atrule&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;configure-catalog-to-load-users&quot;&gt;Configure Catalog to Load Users&lt;/h3&gt;
&lt;p&gt;In &lt;code class=&quot;language-text&quot;&gt;app-config.yaml&lt;/code&gt;, ensure User entities are allowed:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;catalog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Include User and Group in global rules&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Component&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; System&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; API&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Resource&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Location&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Template&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Group&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;locations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; file
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./examples/org.yaml
      &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Group&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-5-environment-variables&quot;&gt;Part 5: Environment Variables&lt;/h2&gt;
&lt;h3 id=&quot;local-development&quot;&gt;Local Development&lt;/h3&gt;
&lt;p&gt;Create a &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; file (add to &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;AUTH_AUTH0_DOMAIN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;dev-xxxxx.us.auth0.com
&lt;span class=&quot;token assign-left variable&quot;&gt;AUTH_AUTH0_CLIENT_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;your-client-id
&lt;span class=&quot;token assign-left variable&quot;&gt;AUTH_AUTH0_CLIENT_SECRET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;your-client-secret
&lt;span class=&quot;token assign-left variable&quot;&gt;AUTH_SESSION_SECRET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;a-random-32-character-string-here&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Backstage doesn&apos;t auto-load &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; files. Either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use &lt;code class=&quot;language-text&quot;&gt;dotenv-cli&lt;/code&gt;: &lt;code class=&quot;language-text&quot;&gt;yarn add -D dotenv-cli&lt;/code&gt; and run &lt;code class=&quot;language-text&quot;&gt;dotenv yarn start-backend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set environment variables directly in your shell&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For PowerShell, add to your profile:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:AUTH_AUTH0_DOMAIN=&lt;span class=&quot;token string&quot;&gt;&quot;dev-xxxxx.us.auth0.com&quot;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:AUTH_AUTH0_CLIENT_ID=&lt;span class=&quot;token string&quot;&gt;&quot;your-client-id&quot;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:AUTH_AUTH0_CLIENT_SECRET=&lt;span class=&quot;token string&quot;&gt;&quot;your-client-secret&quot;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:AUTH_SESSION_SECRET=&lt;span class=&quot;token string&quot;&gt;&quot;your-session-secret&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-6-production-deployment&quot;&gt;Part 6: Production Deployment&lt;/h2&gt;
&lt;h3 id=&quot;add-secrets-to-azure-key-vault&quot;&gt;Add Secrets to Azure Key Vault&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;az keyvault secret &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; --vault-name your-vault &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; AUTH-AUTH0-DOMAIN &lt;span class=&quot;token parameter variable&quot;&gt;--value&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dev-xxxxx.us.auth0.com&quot;&lt;/span&gt;
az keyvault secret &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; --vault-name your-vault &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; AUTH-AUTH0-CLIENT-ID &lt;span class=&quot;token parameter variable&quot;&gt;--value&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-client-id&quot;&lt;/span&gt;
az keyvault secret &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; --vault-name your-vault &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; AUTH-AUTH0-CLIENT-SECRET &lt;span class=&quot;token parameter variable&quot;&gt;--value&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-client-secret&quot;&lt;/span&gt;
az keyvault secret &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; --vault-name your-vault &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; AUTH-&lt;span class=&quot;token environment constant&quot;&gt;SESSION&lt;/span&gt;-SECRET &lt;span class=&quot;token parameter variable&quot;&gt;--value&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;openssl rand &lt;span class=&quot;token parameter variable&quot;&gt;-hex&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;configure-secretproviderclass&quot;&gt;Configure SecretProviderClass&lt;/h3&gt;
&lt;p&gt;Update your CSI Secret Store configuration:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; secrets&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;store.csi.x&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SecretProviderClass
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure
  &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;usePodIdentity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;false&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;useVMManagedIdentity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;false&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;clientID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-managed-identity-client-id&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;keyvaultName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-vault&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;tenantId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-tenant-id&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      array:
        # ... existing secrets
        # Auth0 authentication secrets
        - |
          objectName: AUTH-AUTH0-DOMAIN
          objectType: secret
        - |
          objectName: AUTH-AUTH0-CLIENT-ID
          objectType: secret
        - |
          objectName: AUTH-AUTH0-CLIENT-SECRET
          objectType: secret
        - |
          objectName: AUTH-SESSION-SECRET
          objectType: secret&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;secretObjects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
      &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Opaque
      &lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# ... existing mappings&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Auth0 secrets&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;objectName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;AUTH0&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;DOMAIN
          &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_DOMAIN
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;objectName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;AUTH0&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;CLIENT&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ID
          &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_ID
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;objectName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;AUTH0&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;CLIENT&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;SECRET
          &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_SECRET
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;objectName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;SESSION&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;SECRET
          &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_SESSION_SECRET&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;update-deploymentrollout&quot;&gt;Update Deployment/Rollout&lt;/h3&gt;
&lt;p&gt;Add environment variables to your pod spec:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# Auth0 authentication secrets&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_DOMAIN
    &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_DOMAIN
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_ID
    &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_ID
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_SECRET
    &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_AUTH0_CLIENT_SECRET
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_SESSION_SECRET
    &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AUTH_SESSION_SECRET&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;production-config&quot;&gt;Production Config&lt;/h3&gt;
&lt;p&gt;In &lt;code class=&quot;language-text&quot;&gt;app-config.production.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
  &lt;span class=&quot;token key atrule&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_SESSION_SECRET&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;providers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# No guest provider in production&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;auth0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;clientId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_CLIENT_ID&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;clientSecret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_CLIENT_SECRET&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;AUTH_AUTH0_DOMAIN&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;sessionDuration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hours&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;signIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;resolvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; emailMatchingUserEntityProfileEmail
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; emailLocalPartMatchingUserEntityName

&lt;span class=&quot;token key atrule&quot;&gt;catalog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;locations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Load users from GitHub in production&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; url
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/your&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;org/your&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo/blob/main/backstage/examples/org.yaml
      &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Group&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;testing-the-integration&quot;&gt;Testing the Integration&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Backstage&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; start-backend  &lt;span class=&quot;token comment&quot;&gt;# Terminal 1&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; start          &lt;span class=&quot;token comment&quot;&gt;# Terminal 2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verify User entity loaded&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:7007/api/catalog/entities?filter=kind=user&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Click Auth0 login&lt;/strong&gt; and sign in with a user whose email matches a User entity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check the catalog&lt;/strong&gt; to verify you&apos;re signed in as the correct user&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;There are several moving parts involved in getting Auth0 working with Backstage. Here are some common issues and their solutions.&lt;/p&gt;
&lt;h3 id=&quot;failed-to-sign-in-unable-to-resolve-user-identity&quot;&gt;&quot;Failed to sign-in, unable to resolve user identity&quot;&lt;/h3&gt;
&lt;p&gt;This means Auth0 authenticated successfully, but no matching User entity was found. Check:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;User entity exists&lt;/strong&gt; in catalog (check &lt;code class=&quot;language-text&quot;&gt;/api/catalog/entities?filter=kind=user&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email matches exactly&lt;/strong&gt; between Auth0 profile and User entity &lt;code class=&quot;language-text&quot;&gt;spec.profile.email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catalog rules&lt;/strong&gt; include &lt;code class=&quot;language-text&quot;&gt;User&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;Group&lt;/code&gt; types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait for catalog sync&lt;/strong&gt; after adding new User entities&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;auth0-misconfigured&quot;&gt;&quot;Auth0 misconfigured&quot;&lt;/h3&gt;
&lt;p&gt;Check:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Environment variables are set (not just in &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; file)&lt;/li&gt;
&lt;li&gt;Callback URL in Auth0 matches your Backstage URL&lt;/li&gt;
&lt;li&gt;Domain doesn&apos;t include &lt;code class=&quot;language-text&quot;&gt;https://&lt;/code&gt; prefix&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;guest-option-still-shows-in-production&quot;&gt;Guest option still shows in production&lt;/h3&gt;
&lt;p&gt;Verify &lt;code class=&quot;language-text&quot;&gt;NODE_ENV=production&lt;/code&gt; is set in your deployment. The conditional spread only hides guest when this is set.&lt;/p&gt;
&lt;h3 id=&quot;user-entities-not-loading-from-github-urls&quot;&gt;User entities not loading from GitHub URLs&lt;/h3&gt;
&lt;p&gt;If your catalog logs show:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Unable to read url, NotAllowedError: Reading from &apos;https://raw.githubusercontent.com/...&apos; is not allowed&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You need to add &lt;code class=&quot;language-text&quot;&gt;raw.githubusercontent.com&lt;/code&gt; to the backend reading allow list:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# ... other config&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;reading&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; raw.githubusercontent.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is separate from the GitHub integration. The catalog reader needs its own permission to fetch from raw GitHub URLs.&lt;/p&gt;
&lt;h3 id=&quot;local-file-paths-not-loading-user-entities&quot;&gt;Local file paths not loading User entities&lt;/h3&gt;
&lt;p&gt;File-based catalog locations use paths relative to the config file location, which can sometimes be confusing. Using GitHub raw URLs tends to be more straightforward:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;catalog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;locations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Use raw GitHub URLs instead of local file paths&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; url
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//raw.githubusercontent.com/your&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;org/your&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo/main/backstage/examples/entities.yaml
      &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Group&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;github-rate-limiting&quot;&gt;GitHub rate limiting&lt;/h3&gt;
&lt;p&gt;If catalog processing fails with 403 errors, you&apos;re hitting GitHub&apos;s rate limit. Set &lt;code class=&quot;language-text&quot;&gt;GITHUB_TOKEN&lt;/code&gt; in your environment:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;integrations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;github&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.com
      &lt;span class=&quot;token key atrule&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GITHUB_TOKEN&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Even for public repos, an authenticated token gets 5000 requests/hour vs 60 for anonymous.&lt;/p&gt;
&lt;h3 id=&quot;sqlite-database-errors-in-development&quot;&gt;SQLite database errors in development&lt;/h3&gt;
&lt;p&gt;If you see errors about &lt;code class=&quot;language-text&quot;&gt;connection.filename&lt;/code&gt; not being supported, use the in-memory database:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; better&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;sqlite3
    &lt;span class=&quot;token key atrule&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;:memory:&apos;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Not { filename: &apos;./file.db&apos; }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The new backend system uses a Knex wrapper that handles database connections differently than earlier versions.&lt;/p&gt;
&lt;h3 id=&quot;plugins-blocking-backend-startup&quot;&gt;Plugins blocking backend startup&lt;/h3&gt;
&lt;p&gt;Some plugins (like ArgoCD) will block startup if their external services aren&apos;t configured. For local development, comment them out:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// argocd plugin - requires argocd config, disabled for local dev&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// backend.add(import(&apos;@roadiehq/backstage-plugin-argo-cd-backend&apos;));&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Auth0&apos;s free tier provides a good middle ground between guest authentication and enterprise SSO. The main components are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Backend module&lt;/strong&gt; - handles the OAuth flow and token exchange&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frontend API reference&lt;/strong&gt; - connects the sign-in page to Auth0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sign-in resolvers&lt;/strong&gt; - map Auth0 users to catalog User entities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment configuration&lt;/strong&gt; - keeps development and production settings separate&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach works for both local development and production Kubernetes deployments, with secrets managed through Azure Key Vault.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Building Golden Paths with Backstage: Part 2 - Preview Environments]]></title><description><![CDATA[Automatic ephemeral environments for every pull request: Istio multi-cluster routing, cross-cluster communication, and GitOps-driven preview deployments.]]></description><link>https://chrishouse.io/golden-path-implementation-part-2-preview-environments/</link><guid isPermaLink="false">https://chrishouse.io/golden-path-implementation-part-2-preview-environments/</guid><pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In &lt;a href=&quot;/golden-path-implementation-part-1-foundation&quot;&gt;Part 1&lt;/a&gt;, we built the foundation: a shared development cluster, namespace isolation, and a Backstage template that deploys a running service in under 5 minutes.&lt;/p&gt;
&lt;p&gt;Today we add something developers actually love: &lt;strong&gt;preview environments&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you&apos;ve ever waited for a staging environment to free up so you could test your changes, or merged a PR only to find it broke something obvious that would have been caught with a quick manual test, you understand the problem. Shared staging environments create bottlenecks. Developers queue up behind each other, or worse, deploy over each other&apos;s changes and then spend time debugging phantom issues.&lt;/p&gt;
&lt;p&gt;Preview environments solve this by giving every pull request its own isolated deployment with a unique URL. Code reviewers can click a link in the PR and see the actual running application. Not screenshots, not local recordings, the real thing. QA can test changes before they hit the main branch. Product managers can review features without asking developers to deploy something special for them.&lt;/p&gt;
&lt;p&gt;The concept is simple: PR opens, environment spins up, PR closes, environment disappears. The implementation? That&apos;s where it gets interesting.&lt;/p&gt;
&lt;p&gt;Here&apos;s what we built, and what broke along the way.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-were-building&quot;&gt;What We&apos;re Building&lt;/h2&gt;
&lt;p&gt;When a developer opens a pull request:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GitHub Actions builds the PR branch into a container image&lt;/li&gt;
&lt;li&gt;A GitOps workflow creates an ephemeral namespace&lt;/li&gt;
&lt;li&gt;ArgoCD deploys the preview environment&lt;/li&gt;
&lt;li&gt;A unique URL is generated: &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1-red.chrishouse.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The PR gets a comment with the preview link&lt;/li&gt;
&lt;li&gt;When the PR closes, everything is cleaned up automatically&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The hard part isn&apos;t the workflow. It&apos;s the routing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture-challenge&quot;&gt;The Architecture Challenge&lt;/h2&gt;
&lt;p&gt;Here&apos;s where our design decisions from Part 1 created an interesting puzzle. We have a hub-spoke cluster topology:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hub Cluster&lt;/strong&gt;: Handles all external ingress, runs ArgoCD, Crossplane, Backstage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dev Spoke Cluster&lt;/strong&gt;: Runs application workloads, has no external ingress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation is intentional. The hub cluster is the control plane: it manages infrastructure, handles GitOps, and serves as the single entry point from the internet. The spoke cluster runs actual workloads, isolated from the management plane. It&apos;s a common pattern for enterprise Kubernetes deployments where you want to keep your cattle separate from your pets.&lt;/p&gt;
&lt;p&gt;But preview environments run on the dev spoke. And DNS points to the hub.&lt;/p&gt;
&lt;p&gt;Traffic flow needs to be:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Internet → Hub Istio Gateway → ??? → Dev Spoke → Preview Pod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That middle part is the question mark we need to solve. How do you route traffic from one cluster to another when they&apos;re on different networks? The clusters can talk to each other through VNet peering, but Kubernetes services don&apos;t automatically span clusters. The hub&apos;s Istio gateway has no native way to forward traffic to a service running in a completely different cluster.&lt;/p&gt;
&lt;p&gt;This is a solved problem in the Kubernetes ecosystem. There are several approaches, but each comes with tradeoffs. Let&apos;s walk through what we tried.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-1-the-preview-workflow&quot;&gt;Phase 1: The Preview Workflow&lt;/h2&gt;
&lt;p&gt;Before tackling the routing problem, let&apos;s set up the workflow that will trigger everything. This part is relatively straightforward: a GitHub Actions workflow that fires on pull request events.&lt;/p&gt;
&lt;p&gt;The workflow lives in the Backstage template skeleton, which means every service created through the golden path automatically gets preview environment support. Developers don&apos;t have to configure anything; it just works.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/preview.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Preview Environment

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pull_request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;opened&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; synchronize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reopened&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; closed&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;REGISTRY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io
  &lt;span class=&quot;token key atrule&quot;&gt;SERVICE_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pricing&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
  &lt;span class=&quot;token key atrule&quot;&gt;TEAM_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red
  &lt;span class=&quot;token key atrule&quot;&gt;GITOPS_REPO&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; crh225/ARMServicePortal

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;deploy-preview&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.event.action &lt;span class=&quot;token tag&quot;&gt;!=&lt;/span&gt; &apos;closed&apos;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
      &lt;span class=&quot;token key atrule&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
      &lt;span class=&quot;token key atrule&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v3

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Set environment variables
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; vars
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          echo &quot;pr_number=${{ github.event.pull_request.number }}&quot; &gt;&gt; $GITHUB_OUTPUT
          echo &quot;namespace=${TEAM_NAME}-dev-pr-${{ github.event.pull_request.number }}&quot; &gt;&gt; $GITHUB_OUTPUT
          echo &quot;hostname=${SERVICE_NAME}-pr-${{ github.event.pull_request.number }}-${TEAM_NAME}.chrishouse.io&quot; &gt;&gt; $GITHUB_OUTPUT
          echo &quot;image_tag=pr-${{ github.event.pull_request.number }}&quot; &gt;&gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and push preview image
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;push&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.repository &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.event.pull_request.number &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create GitOps manifests
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          # Creates NamespaceClaim and ArgoCD Application
          # Commits to ARMServicePortal repo
          # ArgoCD discovers and deploys&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The workflow creates two files in the platform repository:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NamespaceClaim&lt;/strong&gt; - Creates the isolated namespace:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform.chrishouse.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NamespaceClaim
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;ephemeral&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;pr-number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;targetCluster&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cluster
    &lt;span class=&quot;token key atrule&quot;&gt;namespaceName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enableResourceQuota&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;ArgoCD Application&lt;/strong&gt; - Deploys the preview:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pricing&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;ephemeral&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/crh225/pricing&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; feature&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;branch
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; helm
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; image.tag
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio.preview.enabled
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio.preview.hostname
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pricing&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;red.chrishouse.io
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cluster
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The workflow uses a personal access token (&lt;code class=&quot;language-text&quot;&gt;GITOPS_TOKEN&lt;/code&gt;) to commit to the platform repository. This feels like a hack but is actually the standard GitOps pattern: your application repo triggers changes in your infrastructure repo, and your GitOps tool (ArgoCD in our case) picks up those changes and applies them.&lt;/p&gt;
&lt;p&gt;This part worked immediately. Within a couple minutes of opening a PR, pods were running and the service was created. The Crossplane NamespaceClaim created the isolated namespace, ArgoCD deployed the application, and we had a working preview environment.&lt;/p&gt;
&lt;p&gt;Except nobody could reach it. Now comes routing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-2-the-routing-problem&quot;&gt;Phase 2: The Routing Problem&lt;/h2&gt;
&lt;p&gt;This is the part that took the longest to solve. Not because the concepts are hard, but because cloud networking has a way of surprising you with limitations that seem arbitrary until you understand the underlying infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;attempt-1-internal-loadbalancer&quot;&gt;Attempt 1: Internal LoadBalancer&lt;/h3&gt;
&lt;p&gt;The obvious first attempt: put an Istio east-west gateway on the dev spoke with an internal (private) LoadBalancer. The hub cluster is on the same Azure VNet peering, so it should be able to reach an internal IP in the dev spoke&apos;s VNet.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LoadBalancer
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;service.beta.kubernetes.io/azure-load-balancer-internal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Hub cluster gets the internal IP. ServiceEntry points to it. VirtualService routes preview traffic.&lt;/p&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; http://10.1.0.158/health
&lt;span class=&quot;token comment&quot;&gt;# ... hangs ...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 100% packet loss&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;: Azure internal LoadBalancers are not accessible across VNet peering by default. The hub cluster (10.0.0.0/16) cannot reach the dev spoke&apos;s internal LoadBalancer (10.1.0.158 in 10.1.0.0/16).&lt;/p&gt;
&lt;p&gt;This was frustrating because everything looked correct. VNet peering was connected and showing &quot;Connected&quot; status in the Azure portal. NSG rules allowed VirtualNetwork traffic in both directions. Route tables looked fine. I spent a good hour checking every setting.&lt;/p&gt;
&lt;p&gt;The issue is that Azure internal LoadBalancers use a different networking path than regular VNet traffic. They&apos;re implemented with a load balancer frontend IP that lives in a special Azure networking plane, and that plane doesn&apos;t traverse VNet peering without additional configuration. The VM-to-VM traffic works fine over peering; the traffic to the LoadBalancer frontend IP doesn&apos;t.&lt;/p&gt;
&lt;p&gt;Azure networking strikes again.&lt;/p&gt;
&lt;h3 id=&quot;attempt-2-azure-private-link-service&quot;&gt;Attempt 2: Azure Private Link Service&lt;/h3&gt;
&lt;p&gt;The Azure-recommended solution for this exact problem is Private Link. You create a Private Link Service that fronts the internal LoadBalancer, then create a Private Endpoint in the hub VNet that connects to that service. Traffic flows through Azure&apos;s backbone, never touching the public internet, and you get a private IP in the hub VNet that routes to the dev spoke&apos;s LoadBalancer.&lt;/p&gt;
&lt;p&gt;Pros: Proper Azure-native cross-VNet LoadBalancer access. Fully private. Works reliably.&lt;/p&gt;
&lt;p&gt;Cons: Adds complexity (two more Azure resources to manage), cost (~$7/month for the private endpoint), and another moving part that can break. For production environments handling sensitive traffic, this is the right answer. For dev preview environments where I&apos;m trying to minimize costs, it felt like overkill.&lt;/p&gt;
&lt;h3 id=&quot;attempt-3-nodeport-with-hardcoded-ips&quot;&gt;Attempt 3: NodePort with Hardcoded IPs&lt;/h3&gt;
&lt;p&gt;I considered bypassing the LoadBalancer entirely. Kubernetes NodePort services expose a port on every node&apos;s IP address. Since VNet peering does work for node-to-node traffic, I could add all the dev spoke&apos;s node IPs to the ServiceEntry.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.1.0.4&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# node1&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.1.0.5&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# node2&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.1.0.6&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# node3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pros: Works with VNet peering. No additional Azure services needed.&lt;/p&gt;
&lt;p&gt;Cons: Hardcoded IPs. The moment the cluster scales up, scales down, or nodes get replaced (which happens regularly with AKS upgrades), this breaks. I&apos;d have to build automation to keep the ServiceEntry in sync with the node pool, and that felt like fighting Kubernetes rather than working with it. Not acceptable for anything beyond a quick test.&lt;/p&gt;
&lt;h3 id=&quot;attempt-4-istio-multi-cluster-with-remote-secrets&quot;&gt;Attempt 4: Istio Multi-Cluster with Remote Secrets&lt;/h3&gt;
&lt;p&gt;Istio was designed for exactly this scenario. Istio&apos;s multi-cluster support allows service meshes to span multiple Kubernetes clusters, with traffic routed seamlessly between them. It is a way how large organizations run Istio across regions, clouds, and network boundaries.&lt;/p&gt;
&lt;p&gt;This is what worked.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-3-istio-multi-cluster&quot;&gt;Phase 3: Istio Multi-Cluster&lt;/h2&gt;
&lt;h3 id=&quot;the-concept&quot;&gt;The Concept&lt;/h3&gt;
&lt;p&gt;Istio multi-cluster is one of those features that sounds complex but solves a real problem elegantly. At its core, it allows Istio&apos;s control plane (istiod) to discover and route to services running in other clusters, as if they were local services.&lt;/p&gt;
&lt;p&gt;The key insight is that cross-cluster communication doesn&apos;t have to be complicated if your service mesh understands the topology. Instead of manually configuring routing rules and endpoints, you tell Istio &quot;here&apos;s another cluster you should know about&quot; and it handles the rest.&lt;/p&gt;
&lt;p&gt;The key components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Remote Secrets&lt;/strong&gt;: Kubeconfig credentials that allow istiod in one cluster to query the Kubernetes API of another cluster. Once istiod can list services and endpoints in the remote cluster, it can route traffic there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;East-West Gateway&lt;/strong&gt;: A dedicated ingress point for cross-cluster traffic. Unlike the north-south gateway that handles external traffic, the east-west gateway handles internal mesh traffic between clusters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Topology Labels&lt;/strong&gt;: Labels on the istio-system namespace that tell Istio which network each cluster belongs to. This helps Istio understand when traffic needs to cross a network boundary.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;implementation&quot;&gt;Implementation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Label namespaces with network topology&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Hub cluster&lt;/span&gt;
kubectl label namespace aks-istio-system &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  topology.istio.io/network&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;hub-network

&lt;span class=&quot;token comment&quot;&gt;# Dev spoke&lt;/span&gt;
kubectl label namespace istio-system &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  topology.istio.io/network&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;shared-dev-network&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create remote secrets&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote secrets allow istiod in one cluster to discover services running in another cluster. The Istio documentation covers this well. Use &lt;code class=&quot;language-text&quot;&gt;istioctl create-remote-secret&lt;/code&gt; to generate the secret for each cluster, then apply it to the other cluster&apos;s Istio namespace.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://istio.io/latest/docs/setup/install/multicluster/&quot;&gt;Istio multi-cluster installation guide&lt;/a&gt; for the complete setup process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Change east-west gateway to public LoadBalancer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s where we make the pragmatic choice. The internal LoadBalancer didn&apos;t work due to Azure&apos;s networking model, and Private Link adds cost and complexity. So we use a public LoadBalancer instead.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# istio-eastwest-gateway-argocd-app.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LoadBalancer
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Removed: azure-load-balancer-internal annotation&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /healthz/ready&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Result: East-west gateway gets public IP &lt;code class=&quot;language-text&quot;&gt;52.255.217.180&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This means the east-west gateway is technically internet-accessible. I&apos;ll discuss the security implications later, but the short version is: for a dev environment with no sensitive data, it&apos;s an acceptable tradeoff.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why public instead of private?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is a cost/complexity tradeoff. The &quot;proper&quot; Azure solution would be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create an Azure Private Link Service exposing the east-west gateway&lt;/li&gt;
&lt;li&gt;Create a Private Endpoint in the hub VNet&lt;/li&gt;
&lt;li&gt;Route through the private endpoint&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That adds ~$7/month for the private endpoint, plus complexity. For a personal lab environment running on my credit card, the public LoadBalancer at ~$3.65/month is acceptable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security implications:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The east-west gateway is now internet-accessible on port 80. However:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It only routes traffic to services with explicit VirtualService configurations&lt;/li&gt;
&lt;li&gt;Services require the correct Host header to match&lt;/li&gt;
&lt;li&gt;No sensitive data is exposed without intentional configuration&lt;/li&gt;
&lt;li&gt;This is a dev environment for preview URLs, not production traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For production environments&lt;/strong&gt;, I&apos;d recommend:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Private Link for fully private cross-cluster routing&lt;/li&gt;
&lt;li&gt;Cilium ClusterMesh when Azure CNI adds support (blocked by &lt;a href=&quot;https://github.com/Azure/AKS/issues/5194&quot;&gt;GitHub issue #5194&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;IP whitelisting on the east-west gateway if public access is required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Configure hub routing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ServiceEntry tells the hub where to find the dev spoke:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.istio.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ServiceEntry
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cluster
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev.internal
  &lt;span class=&quot;token key atrule&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MESH_EXTERNAL
  &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
      &lt;span class=&quot;token key atrule&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; HTTP
  &lt;span class=&quot;token key atrule&quot;&gt;resolution&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; STATIC
  &lt;span class=&quot;token key atrule&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;52.255.217.180&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# East-west gateway public IP&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;VirtualService routes preview traffic to the ServiceEntry:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.istio.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; VirtualService
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; preview&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;envs&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;vs
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.chrishouse.io&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;gateways&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; main&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gateway
  &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;&quot;:authority&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;regex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.*-pr-[0-9]+-[a-z0-9]+\\.chrishouse\\.io&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev.internal
            &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The regex matches preview URLs like &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1-red.chrishouse.io&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Update Helm template for preview VirtualServices&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The application&apos;s VirtualService needs to reference both the hub gateway and the cross-network gateway:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# helm/templates/virtualservice.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.istio.preview.hostname &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;gateways&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.istio.gateway &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.istio.preview.enabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;system/cross&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;network&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gateway
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;verification&quot;&gt;Verification&lt;/h3&gt;
&lt;p&gt;The moment of truth. After all this configuration, does it actually work?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; https://pricing-api-pr-1-red.chrishouse.io/health
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;healthy&quot;&lt;/span&gt;,&lt;span class=&quot;token string&quot;&gt;&quot;service&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pricing-api&quot;&lt;/span&gt;,&lt;span class=&quot;token string&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2025-12-23T15:13:18.004Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That healthy response took way too long to see, but there it is. Traffic is flowing from the internet, through the hub cluster, across the cluster boundary, and into the preview environment running on the dev spoke.&lt;/p&gt;
&lt;p&gt;Here&apos;s the full traffic flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DNS resolution&lt;/strong&gt;: Browser looks up &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1-red.chrishouse.io&lt;/code&gt;, gets &lt;code class=&quot;language-text&quot;&gt;48.194.61.98&lt;/code&gt; (hub Istio ingress)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS termination&lt;/strong&gt;: Hub&apos;s Istio gateway terminates TLS using the &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt; wildcard certificate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern matching&lt;/strong&gt;: Hub&apos;s VirtualService sees the hostname matches the preview regex pattern&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-cluster routing&lt;/strong&gt;: Request is forwarded to ServiceEntry &lt;code class=&quot;language-text&quot;&gt;shared-dev.internal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External routing&lt;/strong&gt;: ServiceEntry resolves to &lt;code class=&quot;language-text&quot;&gt;52.255.217.180&lt;/code&gt; (dev spoke&apos;s east-west gateway)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service routing&lt;/strong&gt;: East-west gateway routes to the pricing-api service based on the Host header&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: The whole chain reverses, response arrives at the browser&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-4-the-tls-certificate-issue&quot;&gt;Phase 4: The TLS Certificate Issue&lt;/h2&gt;
&lt;p&gt;Just when I thought we were done, there was one more surprise waiting.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Testing the preview URL with the original naming scheme:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; https://pricing-api-pr-1.red.chrishouse.io/health
curl: &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; SSL certificate problem: unable to get &lt;span class=&quot;token builtin class-name&quot;&gt;local&lt;/span&gt; issuer certificate&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The routing works (we verified that with HTTP), but HTTPS is failing with a certificate error?&lt;/p&gt;
&lt;p&gt;The original URL pattern was &lt;code class=&quot;language-text&quot;&gt;{service}-pr-{number}.{team}.chrishouse.io&lt;/code&gt;, something like &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1.red.chrishouse.io&lt;/code&gt;. That&apos;s a &lt;strong&gt;two-level subdomain&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1&lt;/code&gt; under &lt;code class=&quot;language-text&quot;&gt;red&lt;/code&gt; under &lt;code class=&quot;language-text&quot;&gt;chrishouse.io&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The existing wildcard certificate is &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt;. And here&apos;s the thing about wildcard certificates that trips people up: they only match &lt;strong&gt;one level&lt;/strong&gt; of subdomain, not arbitrary depth.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pricing-api.chrishouse.io&lt;/code&gt; → matches &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt; ✓&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;anything.chrishouse.io&lt;/code&gt; → matches &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt; ✓&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1.red.chrishouse.io&lt;/code&gt; → does NOT match &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt; ✗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The wildcard only replaces the single &lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt; portion. It doesn&apos;t recursively match nested subdomains.&lt;/p&gt;
&lt;h3 id=&quot;options-considered&quot;&gt;Options Considered&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Option A: Per-team wildcard certs&lt;/strong&gt; (&lt;code class=&quot;language-text&quot;&gt;*.red.chrishouse.io&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;I could create a wildcard certificate for each team&apos;s subdomain. &lt;code class=&quot;language-text&quot;&gt;*.red.chrishouse.io&lt;/code&gt; would match &lt;code class=&quot;language-text&quot;&gt;pricing-api-pr-1.red.chrishouse.io&lt;/code&gt; just fine.&lt;/p&gt;
&lt;p&gt;Rejected. That means managing N certificates where N is the number of teams. Each new team requires provisioning a new certificate, configuring it in the gateway, and keeping track of renewals. Certificate management is already tedious; multiplying it doesn&apos;t help.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option B: SAN certificate with all patterns&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A single certificate with Subject Alternative Names (SANs) for each team pattern: &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.red.chrishouse.io&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.blue.chrishouse.io&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;Rejected. Same problem: the certificate needs to be regenerated every time a new team is added. Plus there are limits on SAN entries, and it adds operational overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option C: Change URL pattern to single-level subdomain&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of &lt;code class=&quot;language-text&quot;&gt;{service}-pr-{number}.{team}.chrishouse.io&lt;/code&gt;, use &lt;code class=&quot;language-text&quot;&gt;{service}-pr-{number}-{team}.chrishouse.io&lt;/code&gt;. The team name becomes part of the single subdomain rather than its own level.&lt;/p&gt;
&lt;p&gt;Accepted. It&apos;s the simplest solution and works with the existing wildcard certificate without any changes to certificate management.&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The Fix&lt;/h3&gt;
&lt;p&gt;Changed URL pattern from:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;{service}-pr-{number}.{team}.chrishouse.io  (two-level)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;{service}-pr-{number}-{team}.chrishouse.io  (single-level)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Updated files:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;backstage/templates/nodejs-quickstart/skeleton/.github/workflows/preview.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;backstage/templates/nodejs-quickstart/skeleton/helm/values.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;infra/kubernetes/istio-hub/virtualservice-preview-envs.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pricing-api/.github/workflows/preview.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;New regex pattern:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;&quot;:authority&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;regex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.*-pr-[0-9]+-[a-z0-9]+\\.chrishouse\\.io&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;dns-update&quot;&gt;DNS Update&lt;/h3&gt;
&lt;p&gt;The final piece of the puzzle was DNS. I updated the wildcard DNS record &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt; to point to the hub Istio ingress at &lt;code class=&quot;language-text&quot;&gt;48.194.61.98&lt;/code&gt;. This means any subdomain that doesn&apos;t have a more specific A record will resolve to the hub gateway.&lt;/p&gt;
&lt;p&gt;Importantly, existing services with explicit A records (like &lt;code class=&quot;language-text&quot;&gt;argohub.chrishouse.io&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;backstage.chrishouse.io&lt;/code&gt; pointing to nginx-ingress at &lt;code class=&quot;language-text&quot;&gt;20.253.73.108&lt;/code&gt;) are unaffected. DNS resolution prefers more specific records over wildcards, so those services continue to work exactly as before.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-5-automatic-cleanup&quot;&gt;Phase 5: Automatic Cleanup&lt;/h2&gt;
&lt;p&gt;Preview environments are only useful if they don&apos;t accumulate. Without cleanup, you&apos;d end up with dozens of abandoned namespaces consuming cluster resources, each one a forgotten artifact of a PR from three months ago.&lt;/p&gt;
&lt;p&gt;When a PR is merged or closed, the preview environment should be deleted automatically. This is the &quot;ephemeral&quot; part of ephemeral environments.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;cleanup-preview&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.event.action == &apos;closed&apos;
  &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout GitOps repository
      &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v3
      &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; crh225/ARMServicePortal
        &lt;span class=&quot;token key atrule&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITOPS_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gitops

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Remove preview environment manifests
      &lt;span class=&quot;token key atrule&quot;&gt;working-directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gitops
      &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        rm -rf infra/quickstart-services/${SERVICE_NAME}/preview-pr-${PR_NUM}&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Commit and push cleanup
      &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        git add .
        git commit -m &quot;Cleanup preview environment for PR #${PR_NUM}&quot;
        git push&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The cleanup follows the same GitOps pattern as creation. The workflow deletes the manifests from Git, commits the change, and lets ArgoCD handle the rest. ArgoCD has &lt;code class=&quot;language-text&quot;&gt;prune: true&lt;/code&gt; in its sync policy, which means when it detects that a resource exists in the cluster but not in Git, it deletes it.&lt;/p&gt;
&lt;p&gt;This is one of the elegant things about GitOps: cleanup is just another commit. Namespace, pods, services, VirtualService, all removed automatically when the ArgoCD Application manifest disappears from the repository. No custom cleanup scripts, no cron jobs scanning for orphaned resources, no manual intervention.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-final-architecture&quot;&gt;The Final Architecture&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Internet
    │
    ▼
┌─────────────────────────────────────────┐
│ Hub Cluster (aks-mgmt-hub)              │
│                                         │
│ ┌─────────────────────────────────────┐ │
│ │ Istio Gateway (48.194.61.98)        │ │
│ │ - TLS termination (*.chrishouse.io) │ │
│ │ - VirtualService regex matching     │ │
│ └──────────────┬──────────────────────┘ │
│                │                        │
│ ┌──────────────▼──────────────────────┐ │
│ │ ServiceEntry (shared-dev.internal)  │ │
│ │ → 52.255.217.180                    │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────┐
│ Dev Spoke (aks-shared-dev)              │
│                                         │
│ ┌─────────────────────────────────────┐ │
│ │ East-West Gateway (52.255.217.180)  │ │
│ │ - Public LoadBalancer               │ │
│ └──────────────┬──────────────────────┘ │
│                │                        │
│ ┌──────────────▼──────────────────────┐ │
│ │ VirtualService (pricing-api)        │ │
│ │ - Routes to service in namespace    │ │
│ └──────────────┬──────────────────────┘ │
│                │                        │
│ ┌──────────────▼──────────────────────┐ │
│ │ pricing-api Service                 │ │
│ │ Namespace: red-dev-pr-1             │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;/diagrams/preview-environments&quot;&gt;View interactive diagram →&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-we-learned&quot;&gt;What We Learned&lt;/h2&gt;
&lt;p&gt;Building this feature took longer than expected, but most of the time wasn&apos;t spent on the preview workflow itself (that was straightforward). The complexity was in the cross-cluster routing, and specifically in working around Azure&apos;s networking limitations.&lt;/p&gt;
&lt;h3 id=&quot;what-worked&quot;&gt;What Worked&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitOps-driven preview environments&lt;/strong&gt;: The pattern of PR workflow → GitOps commit → ArgoCD deploy → cleanup commit is clean and reliable. Every state change is recorded in Git, which makes debugging and auditing trivial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Istio multi-cluster with remote secrets&lt;/strong&gt;: Once configured, this just works. Istio handles service discovery across clusters without any per-service configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-level subdomain URLs&lt;/strong&gt;: A simple URL pattern change avoided certificate complexity entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic cleanup&lt;/strong&gt;: GitOps makes cleanup as reliable as deployment. If the manifest isn&apos;t in Git, the resource doesn&apos;t exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-didnt-work-initially&quot;&gt;What Didn&apos;t Work (Initially)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Azure internal LoadBalancers&lt;/strong&gt;: Not accessible across VNet peering without Private Link. This was a frustrating discovery because everything else about VNet peering works fine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-level subdomain URLs&lt;/strong&gt;: Wildcard certs only match one level. This is well-documented behavior, but easy to forget when designing URL schemes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;key-decisions&quot;&gt;Key Decisions&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cross-cluster routing&lt;/td&gt;
&lt;td&gt;Istio multi-cluster&lt;/td&gt;
&lt;td&gt;Industry standard, mTLS, service discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;East-west gateway&lt;/td&gt;
&lt;td&gt;Public LoadBalancer&lt;/td&gt;
&lt;td&gt;Azure internal LBs not accessible cross-VNet; cheaper than Private Link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL pattern&lt;/td&gt;
&lt;td&gt;Single-level subdomain&lt;/td&gt;
&lt;td&gt;Works with existing wildcard cert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup trigger&lt;/td&gt;
&lt;td&gt;PR close event&lt;/td&gt;
&lt;td&gt;Immediate, no TTL complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;cost-vs-security-tradeoff&quot;&gt;Cost vs Security Tradeoff&lt;/h3&gt;
&lt;p&gt;This is a personal lab environment running on my Azure subscription (and credit card). The architecture choices reflect that:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Security&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Public LoadBalancer&lt;/strong&gt; (chosen)&lt;/td&gt;
&lt;td&gt;~$3.65&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Dev-acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Private Link&lt;/td&gt;
&lt;td&gt;~$10.65&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Production-ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPN/ExpressRoute&lt;/td&gt;
&lt;td&gt;$50+&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Enterprise-grade&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For production workloads, we could use Azure Private Link. For a dev environment where the only exposed services are ephemeral preview deployments with no sensitive data, the public LoadBalancer is a reasonable tradeoff.&lt;/p&gt;
&lt;p&gt;The east-west gateway only routes traffic to services with explicit VirtualService configurations. It&apos;s not an open proxy.&lt;/p&gt;
&lt;h3 id=&quot;operational-costs&quot;&gt;Operational Costs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;East-west gateway public IP: ~$3.65/month (Azure)&lt;/li&gt;
&lt;li&gt;Additional Istio overhead: Minimal (already running Istio on both clusters)&lt;/li&gt;
&lt;li&gt;Network egress: Hub → dev spoke traffic stays within Azure&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;implementation-repository&quot;&gt;Implementation Repository&lt;/h2&gt;
&lt;p&gt;Full implementation: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal&quot;&gt;github.com/crh225/ARMServicePortal&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Key files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Preview workflow template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/backstage/templates/nodejs-quickstart/skeleton/.github/workflows/preview.yml&quot;&gt;backstage/templates/nodejs-quickstart/skeleton/.github/workflows/preview.yml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Hub VirtualService: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/infra/kubernetes/istio-hub/virtualservice-preview-envs.yaml&quot;&gt;infra/kubernetes/istio-hub/virtualservice-preview-envs.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ServiceEntry: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/infra/kubernetes/istio-hub/service-entry-shared-dev.yaml&quot;&gt;infra/kubernetes/istio-hub/service-entry-shared-dev.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;East-west gateway: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/infra/cluster-bootstrap-shared-dev/istio-eastwest-gateway-argocd-app.yaml&quot;&gt;infra/cluster-bootstrap-shared-dev/istio-eastwest-gateway-argocd-app.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;VirtualService Helm template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/backstage/templates/nodejs-quickstart/skeleton/helm/templates/virtualservice.yaml&quot;&gt;backstage/templates/nodejs-quickstart/skeleton/helm/templates/virtualservice.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Next in series:&lt;/strong&gt; Part 3 will cover cost visibility, showing developers the real cost of their applications directly in Backstage with Azure Cost Management integration and resource tagging.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Building Golden Paths with Backstage: Part 1 - Foundation]]></title><description><![CDATA[Building a working golden path from concept to production: shared development clusters, namespace isolation, and end-to-end GitOps deployment with Backstage.]]></description><link>https://chrishouse.io/golden-path-implementation-part-1-foundation/</link><guid isPermaLink="false">https://chrishouse.io/golden-path-implementation-part-1-foundation/</guid><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the first in a multi-part series documenting the actual implementation of a golden path for microservices deployment. Not the theory, but the real commands, the actual errors, and what it takes to go from empty cluster to working self-service platform.&lt;/p&gt;
&lt;p&gt;There&apos;s no shortage of conference talks about platform engineering. Plenty of blog posts explaining why golden paths matter. What&apos;s harder to find is someone walking through the actual implementation: the YAML files, the permission errors, the &quot;why isn&apos;t this working&quot; moments that don&apos;t make it into the polished demos.&lt;/p&gt;
&lt;p&gt;That&apos;s what this series is. By the end of today&apos;s work: a developer fills in 4 fields in Backstage, and 5 minutes later has a running microservice deployed to Kubernetes with health checks responding.&lt;/p&gt;
&lt;p&gt;Here&apos;s how we built it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-were-building&quot;&gt;What We&apos;re Building&lt;/h2&gt;
&lt;p&gt;A self-service platform where developers can deploy microservices without:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manually creating Kubernetes namespaces&lt;/li&gt;
&lt;li&gt;Configuring CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Setting up container registries&lt;/li&gt;
&lt;li&gt;Writing ArgoCD applications&lt;/li&gt;
&lt;li&gt;Requesting infrastructure tickets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal: minimize the distance between &quot;I want to deploy a service&quot; and &quot;my service is running in production.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-1-shared-development-cluster-foundation&quot;&gt;Phase 1: Shared Development Cluster Foundation&lt;/h2&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Every team wants their own Kubernetes cluster. Understandable: clean isolation, no noisy neighbors, full control.&lt;/p&gt;
&lt;p&gt;It&apos;s also expensive, operationally complex, and usually overkill for development workloads.&lt;/p&gt;
&lt;p&gt;The alternative: one shared development cluster with proper namespace isolation. Teams get their own space, resource limits prevent noisy neighbor problems, and operations stays sane. The tradeoff is that you need to actually implement the isolation properly, which is what this section covers.&lt;/p&gt;
&lt;h3 id=&quot;the-infrastructure-stack&quot;&gt;The Infrastructure Stack&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cluster:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Kubernetes Service (AKS)&lt;/li&gt;
&lt;li&gt;Named &lt;code class=&quot;language-text&quot;&gt;aks-shared-dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Single cluster for all development teams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Namespace Management:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Crossplane for declarative namespace provisioning&lt;/li&gt;
&lt;li&gt;Custom Resource Definition: &lt;code class=&quot;language-text&quot;&gt;XNamespaceClaim&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teams request namespaces, Crossplane creates them with isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Isolation Mechanisms:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Resource quotas per team namespace&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;resourceQuota&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;requestsCpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;requestsMemory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4Gi&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;limitsCpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;limitsMemory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;8Gi&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Default limits for containers&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;limitRange&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;cpuRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100m&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;cpuLimit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;500m&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;memoryRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;128Mi&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;memoryLimit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;512Mi&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Network policies enabled&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;enableNetworkPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each team gets a namespace with hard resource limits. No single team can consume the entire cluster. This sounds obvious, but without these limits, one team&apos;s runaway process or misconfigured HPA can starve everyone else.&lt;/p&gt;
&lt;h3 id=&quot;why-crossplane-instead-of-kubectl&quot;&gt;Why Crossplane Instead of kubectl?&lt;/h3&gt;
&lt;p&gt;We could just run &lt;code class=&quot;language-text&quot;&gt;kubectl create namespace red-dev&lt;/code&gt; and be done. It&apos;s one command. Why introduce another abstraction layer?&lt;/p&gt;
&lt;p&gt;I had this debate with myself for longer than I&apos;d like to admit. The answer comes down to what happens after day one. Creating a namespace is easy. Creating a namespace with the right resource quotas, limit ranges, network policies, RBAC bindings, and labels every single time, consistently, without forgetting anything? That&apos;s where things fall apart.&lt;/p&gt;
&lt;p&gt;Instead, we use Crossplane to create a &lt;code class=&quot;language-text&quot;&gt;NamespaceClaim&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform.chrishouse.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NamespaceClaim
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; crossplane&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;system
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev
    &lt;span class=&quot;token key atrule&quot;&gt;created-by&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;targetCluster&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cluster
    &lt;span class=&quot;token key atrule&quot;&gt;namespaceName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev
    &lt;span class=&quot;token key atrule&quot;&gt;enableResourceQuota&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enableNetworkPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enableLimitRange&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Why the extra abstraction?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt; - Every namespace gets resource quotas, limit ranges, and network policies automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitOps&lt;/strong&gt; - Namespace configuration is declarative and version-controlled&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-service&lt;/strong&gt; - Backstage templates can create namespaces without cluster credentials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditability&lt;/strong&gt; - Clear record of who requested what and when&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Crossplane composition handles the actual Kubernetes API calls. Teams just declare what they need. The composition is essentially a contract: you give me a namespace name and a team label, I give you a fully configured namespace with all the guardrails in place.&lt;/p&gt;
&lt;h3 id=&quot;verification&quot;&gt;Verification&lt;/h3&gt;
&lt;p&gt;After setting up the cluster and Crossplane:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get namespaceclaim &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; crossplane-system
NAME      SYNCED   READY   AGE
red-dev   True     True    15m

$ kubectl get namespace red-dev &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml
apiVersion: v1
kind: Namespace
metadata:
  labels:
    team: red
    environment: dev
    managed-by: crossplane
  name: red-dev

$ kubectl get resourcequota &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; red-dev
NAME                AGE
red-dev-quota       15m

$ kubectl get limitrange &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; red-dev
NAME                AGE
red-dev-limits      15m&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Namespace exists. Quotas applied. Limits enforced. This might not look like much, but it&apos;s the foundation everything else builds on. Get this wrong, and you&apos;ll be debugging resource contention issues and namespace configuration drift for months.&lt;/p&gt;
&lt;p&gt;Phase 1 complete.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;phase-2-microservice-deployment-template&quot;&gt;Phase 2: Microservice Deployment Template&lt;/h2&gt;
&lt;h3 id=&quot;the-goal&quot;&gt;The Goal&lt;/h3&gt;
&lt;p&gt;Now we have a place to put things. The next question: how do developers actually get their code running there?&lt;/p&gt;
&lt;p&gt;The developer experience should be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &quot;Create Component&quot; in Backstage&lt;/li&gt;
&lt;li&gt;Fill in: Service Name, Team, Description, Owner&lt;/li&gt;
&lt;li&gt;Wait 5 minutes&lt;/li&gt;
&lt;li&gt;Service is running&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything else happens automatically. No terminal. No YAML editing. No waiting for someone in ops to process a ticket.&lt;/p&gt;
&lt;h3 id=&quot;what-everything-else-means&quot;&gt;What &quot;Everything Else&quot; Means&lt;/h3&gt;
&lt;p&gt;That &quot;everything else&quot; is doing a lot of heavy lifting. Behind that 4-field form, the platform needs to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create GitHub repository with proper structure&lt;/li&gt;
&lt;li&gt;Configure repository permissions for GHCR (GitHub Container Registry)&lt;/li&gt;
&lt;li&gt;Set up CI/CD pipeline to build and publish container images&lt;/li&gt;
&lt;li&gt;Create namespace in shared dev cluster (via Crossplane)&lt;/li&gt;
&lt;li&gt;Create ArgoCD application manifest&lt;/li&gt;
&lt;li&gt;Commit GitOps configuration to platform repo&lt;/li&gt;
&lt;li&gt;Let ArgoCD discover and deploy the service&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&apos;s seven separate systems that need to coordinate correctly. Any one of them failing silently would leave developers confused about why their service isn&apos;t running. This is where the abstraction of &quot;fill in a form&quot; starts looking a lot more complex than it sounds.&lt;/p&gt;
&lt;p&gt;Let&apos;s break it down.&lt;/p&gt;
&lt;h3 id=&quot;the-backstage-template&quot;&gt;The Backstage Template&lt;/h3&gt;
&lt;p&gt;Core structure:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scaffolder.backstage.io/v1beta3
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Template
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nodejs&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;microservice&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;quickstart
  &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Node.js Microservice (Quick Start)
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service Information
      &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;service_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service Name
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
        &lt;span class=&quot;token key atrule&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Team Name
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Description
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
        &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Owner
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Four input fields. That&apos;s it. Everything else is either defaulted or derived from these values. The temptation to add more fields is strong, but every additional input is friction. Resist it.&lt;/p&gt;
&lt;h3 id=&quot;template-steps&quot;&gt;Template Steps&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Scaffold Repository&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;template
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Fetch Template
  &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;template
  &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./skeleton
    &lt;span class=&quot;token key atrule&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;serviceName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.team &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.description &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Backstage copies the template skeleton and replaces variables.&lt;/p&gt;
&lt;p&gt;The skeleton includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js application with Express&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Dockerfile&lt;/code&gt; for container builds&lt;/li&gt;
&lt;li&gt;GitHub Actions workflow for CI/CD&lt;/li&gt;
&lt;li&gt;Helm chart for Kubernetes deployment&lt;/li&gt;
&lt;li&gt;Health check endpoints (&lt;code class=&quot;language-text&quot;&gt;/health&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/ready&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create GitHub Repository&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; publish&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;github
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Publish to GitHub
  &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; publish&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;github
  &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.com&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;owner=crh225&lt;span class=&quot;token important&quot;&gt;&amp;amp;repo=$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;defaultBranch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Configure Repository Permissions&lt;/strong&gt; (Critical Step)&lt;/p&gt;
&lt;p&gt;This is where it gets interesting, and where I wasted more time than I&apos;d like to admit.&lt;/p&gt;
&lt;p&gt;GitHub Container Registry requires specific repository permissions. By default, new repositories have workflow permissions set to &lt;code class=&quot;language-text&quot;&gt;read&lt;/code&gt;. This seems fine until you try to push a container image. The build passes, the login succeeds, and then:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;denied: permission_denied: write_package&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The error message is unhelpful. You&apos;ll search for PAT token issues, GHCR authentication problems, Docker login failures. None of that is the problem. The problem is a single checkbox in the repository settings that nobody told you about.&lt;/p&gt;
&lt;p&gt;We could fix this manually for each repository, but that defeats the purpose of automation. Better: automate it with a custom Backstage action that configures permissions immediately after repository creation.&lt;/p&gt;
&lt;p&gt;Custom action: &lt;code class=&quot;language-text&quot;&gt;armportal:github:configure-repo&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;configureGitHubRepoAction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; token&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createTemplateAction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;armportal:github:configure-repo&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; repoUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; token &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; owner&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; repoUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;github\.com\?.*owner=([^&amp;amp;]+).*repo=([^&amp;amp;]+)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; octokit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Octokit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; token &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// Set workflow permissions to &apos;write&apos;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; octokit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;PUT /repos/{owner}/{repo}/actions/permissions/workflow&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        owner&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        default_workflow_permissions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;write&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        can_approve_pull_request_reviews&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// Enable GitHub Actions&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; octokit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;PUT /repos/{owner}/{repo}/actions/permissions&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        owner&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        allowed_actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;all&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Used in template:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Configure Repository Permissions
  &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; armportal&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;github&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo
  &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.com&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;owner=crh225&lt;span class=&quot;token important&quot;&gt;&amp;amp;repo=$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now GHCR publishing works automatically. Every new repository gets the right permissions from the start. No manual intervention, no mysterious failures on the first build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Create GitOps Configuration&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gitops&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create GitOps PR
  &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; armportal&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr
  &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.com&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;owner=crh225&lt;span class=&quot;token important&quot;&gt;&amp;amp;repo=ARMServicePortal&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Add ${{ parameters.service_name }} to platform&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; infra/crossplane/claims/aks&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.team &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev/namespace&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;claim.yaml
        &lt;span class=&quot;token key atrule&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;prepare-gitops&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;.output.namespaceClaim &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; infra/quickstart&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;services/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.service_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/argocd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application.yaml
        &lt;span class=&quot;token key atrule&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;prepare-gitops&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;.output.argoApplication &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This creates a pull request in the platform repository with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Namespace claim for Crossplane&lt;/li&gt;
&lt;li&gt;ArgoCD application manifest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Merge the PR. GitOps takes over. This separation matters: the template creates the intent (a PR), but a human approves the actual deployment. You can make this automatic if you want, but having that gate gives teams a moment to review what&apos;s about to happen.&lt;/p&gt;
&lt;h3 id=&quot;the-cicd-pipeline&quot;&gt;The CI/CD Pipeline&lt;/h3&gt;
&lt;p&gt;The template generates a complete GitHub Actions workflow for each new service. Nothing fancy here, just the standard build-and-push pattern:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and Push to GHCR

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
      &lt;span class=&quot;token key atrule&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v3

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Convert repository name to lowercase
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; repo
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; echo &quot;repository=$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.repository &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; tr &apos;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;upper&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&apos; &apos;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;lower&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&apos; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; $GITHUB_OUTPUT

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to GitHub Container Registry
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/login&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v2
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io
          &lt;span class=&quot;token key atrule&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.actor &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and push
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;push&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
          &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps.repo.outputs.repository &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Key detail: lowercase repository name. GHCR requires it, and GitHub repository names can have uppercase letters. This mismatch will cause failures if you forget to normalize. The workflow handles it with that &lt;code class=&quot;language-text&quot;&gt;tr&lt;/code&gt; command, but I learned this the hard way after debugging &quot;image not found&quot; errors for an embarrassingly long time.&lt;/p&gt;
&lt;h3 id=&quot;argocd-service-discovery&quot;&gt;ArgoCD Service Discovery&lt;/h3&gt;
&lt;p&gt;Problem: Every time we create a new service, we&apos;d need to manually register it with ArgoCD. That&apos;s exactly the kind of manual step that makes developers go around the platform.&lt;/p&gt;
&lt;p&gt;Solution: App-of-Apps pattern. ArgoCD watches a directory. When new files appear, it creates applications automatically.&lt;/p&gt;
&lt;p&gt;Created &lt;code class=&quot;language-text&quot;&gt;infra/argocd/apps/quickstart-services-app.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; quickstart&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;services
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; applications
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/crh225/ARMServicePortal.git
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; infra/quickstart&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;services
    &lt;span class=&quot;token key atrule&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;recurse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;*/argocd-application.yaml&apos;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
  &lt;span class=&quot;token key atrule&quot;&gt;syncPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;automated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;prune&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;selfHeal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ArgoCD watches &lt;code class=&quot;language-text&quot;&gt;infra/quickstart-services/&lt;/code&gt;. When it finds a new &lt;code class=&quot;language-text&quot;&gt;argocd-application.yaml&lt;/code&gt;, it creates the application automatically. The template commits the application manifest, and within a few minutes ArgoCD notices the new file and deploys the service.&lt;/p&gt;
&lt;p&gt;No manual ArgoCD registration required. No &quot;please add my app to ArgoCD&quot; tickets.&lt;/p&gt;
&lt;h3 id=&quot;the-first-error-servicemonitor-crd-missing&quot;&gt;The First Error: ServiceMonitor CRD Missing&lt;/h3&gt;
&lt;p&gt;Everything was working. Repository created, permissions configured, image built and pushed. ArgoCD picked up the application manifest and started syncing.&lt;/p&gt;
&lt;p&gt;Then it failed. ArgoCD showed:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;OutOfSync
Missing

Error: failed to discover server resources for group version monitoring.coreos.com/v1:
the server could not find the requested resource&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The template included a ServiceMonitor resource for Prometheus metrics, because of course you want metrics for your services. But Prometheus Operator wasn&apos;t installed on the cluster yet. The CRD didn&apos;t exist. ArgoCD couldn&apos;t apply something that the cluster didn&apos;t understand.&lt;/p&gt;
&lt;p&gt;This is a common trap with golden path templates: including &quot;best practices&quot; resources that depend on infrastructure that doesn&apos;t exist yet. The fix is simple, but the lesson is important. Don&apos;t put aspirational resources in your default template. :D&lt;/p&gt;
&lt;p&gt;Fix: Disable ServiceMonitor by default in template.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# helm/values.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;serviceMonitor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Set to true after installing Prometheus Operator&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Re-sync. Success. Enable it later when Prometheus is actually running.&lt;/p&gt;
&lt;h3 id=&quot;verification-pricing-api-deployment&quot;&gt;Verification: pricing-api Deployment&lt;/h3&gt;
&lt;p&gt;Time to test the whole pipeline end-to-end. I created &lt;code class=&quot;language-text&quot;&gt;pricing-api&lt;/code&gt; via the Backstage template, filled in the four fields, and waited:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; red-dev
NAME                          READY   STATUS    RESTARTS   AGE
pricing-api-6f4b8d9c7-8xk2p   &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;/1     Running   &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;          2m
pricing-api-6f4b8d9c7-m7n4q   &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;/1     Running   &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;          2m

$ kubectl port-forward &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; red-dev svc/pricing-api &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:80
Forwarding from &lt;span class=&quot;token number&quot;&gt;127.0&lt;/span&gt;.0.1:8080 -&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;

$ &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; http://localhost:8080/health
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;healthy&quot;&lt;/span&gt;,&lt;span class=&quot;token string&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2025-12-22T15:30:00.000Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two pods running. Health checks responding. The entire chain worked: Backstage created the repo, configured GHCR permissions, GitHub Actions built and pushed the image, the GitOps PR got merged, ArgoCD deployed the application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Time from Backstage form submission to running service: 4 minutes, 37 seconds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That number matters. If this took 30 minutes, developers would start looking for shortcuts. If it took 2 days waiting for approvals, they&apos;d definitely route around the platform. Under 5 minutes is fast enough that the golden path becomes the path of least resistance.&lt;/p&gt;
&lt;p&gt;Phase 2 complete.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/diagrams/golden-path-foundation&quot;&gt;View interactive diagrams →&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-actually-happened&quot;&gt;What Actually Happened&lt;/h2&gt;
&lt;p&gt;Starting from an empty shared development cluster, we built:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Crossplane namespace provisioning with resource quotas&lt;/li&gt;
&lt;li&gt;Shared AKS cluster for all development teams&lt;/li&gt;
&lt;li&gt;Namespace isolation with limits and network policies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Automation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backstage template for Node.js microservices&lt;/li&gt;
&lt;li&gt;Custom action for automatic GHCR permissions&lt;/li&gt;
&lt;li&gt;CI/CD pipeline for container builds&lt;/li&gt;
&lt;li&gt;ArgoCD App-of-Apps for service discovery&lt;/li&gt;
&lt;li&gt;GitOps workflow for all deployments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Developer Experience:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 input fields in Backstage&lt;/li&gt;
&lt;li&gt;~5 minutes to running service&lt;/li&gt;
&lt;li&gt;No manual configuration required&lt;/li&gt;
&lt;li&gt;No cluster credentials needed&lt;/li&gt;
&lt;li&gt;No infrastructure tickets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;End-to-end automation from form to deployment&lt;/li&gt;
&lt;li&gt;Automatic GHCR configuration&lt;/li&gt;
&lt;li&gt;Service discovery via App-of-Apps pattern&lt;/li&gt;
&lt;li&gt;Resource isolation in shared cluster&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What didn&apos;t (initially):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServiceMonitor CRD dependency&lt;/li&gt;
&lt;li&gt;Repository name case sensitivity in GHCR&lt;/li&gt;
&lt;li&gt;ArgoCD not watching quickstart-services directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All fixed. System working. Each of these issues took time to debug, but once fixed, they stay fixed. Every developer who uses the template after this benefits from the lessons learned.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&apos;s Next&lt;/h2&gt;
&lt;p&gt;This gets us from zero to deployed service. A developer can fill out a form and have a running microservice in under 5 minutes. But we&apos;re missing some important pieces:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Networking:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;External ingress (currently ClusterIP only)&lt;/li&gt;
&lt;li&gt;Istio service mesh for mTLS&lt;/li&gt;
&lt;li&gt;DNS configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Developer Workflow:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Preview environments per pull request&lt;/li&gt;
&lt;li&gt;Automated testing in pipelines&lt;/li&gt;
&lt;li&gt;Rollback mechanisms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Policy enforcement (OPA/Kyverno)&lt;/li&gt;
&lt;li&gt;Secret management (External Secrets Operator)&lt;/li&gt;
&lt;li&gt;Image scanning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those are the next phases. Each one adds complexity, and each one needs to be optional until it&apos;s required.&lt;/p&gt;
&lt;p&gt;The principle remains: make the golden path faster than the alternative.&lt;/p&gt;
&lt;p&gt;If creating a service this way takes 5 minutes, and the manual alternative takes days of tickets and approvals, developers will use the platform.&lt;/p&gt;
&lt;p&gt;If the platform becomes slower or more restrictive than doing it manually, they&apos;ll route around it.&lt;/p&gt;
&lt;p&gt;That&apos;s the balance we&apos;re building toward.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;implementation-repository&quot;&gt;Implementation Repository&lt;/h2&gt;
&lt;p&gt;Full implementation available at: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal&quot;&gt;github.com/crh225/ARMServicePortal&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Key files referenced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backstage template: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/backstage/templates/nodejs-quickstart/template.yaml&quot;&gt;backstage/templates/nodejs-quickstart/template.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Custom GitHub action: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/backstage/plugins/arm-portal-backend/src/scaffolder/actions/configureGitHubRepo.ts&quot;&gt;backstage/plugins/arm-portal-backend/src/scaffolder/actions/configureGitHubRepo.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;App-of-Apps: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/infra/argocd/apps/quickstart-services-app.yaml&quot;&gt;infra/argocd/apps/quickstart-services-app.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Crossplane composition: &lt;a href=&quot;https://github.com/crh225/ARMServicePortal/blob/main/infra/crossplane/platform/namespace-composition.yaml&quot;&gt;infra/crossplane/platform/namespace-composition.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Next in series:&lt;/strong&gt; Part 2 will cover preview environments: automatically creating ephemeral deployments for every pull request, with DNS, TLS certificates, and automatic cleanup.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Platform Engineering Golden Paths: Common Patterns]]></title><description><![CDATA[Examining the three-tier abstraction model, progressive disclosure, and environment lifecycle patterns that appear across platform engineering implementations.]]></description><link>https://chrishouse.io/platform-engineering-golden-paths-industry-patterns/</link><guid isPermaLink="false">https://chrishouse.io/platform-engineering-golden-paths-industry-patterns/</guid><pubDate>Sun, 21 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have been thinking about how to build Self-service developer platforms, and deal with trade-offs: how much infrastructure complexity to expose, how to serve different user needs, and how to balance flexibility with standardization.&lt;/p&gt;
&lt;p&gt;Looking at various implementations—from open-source tools like Backstage&lt;sup&gt;&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; to commercial platforms like Heroku&lt;sup&gt;&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; and Vercel&lt;sup&gt;&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;—some recurring patterns emerge.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-three-tier-abstraction-model&quot;&gt;The Three-Tier Abstraction Model&lt;/h2&gt;
&lt;p&gt;Many platforms structure their interfaces around three levels of abstraction, each serving different use cases.&lt;/p&gt;
&lt;h3 id=&quot;tier-1-minimal-input-templates&quot;&gt;Tier 1: Minimal Input Templates&lt;/h3&gt;
&lt;p&gt;Backstage&apos;s software templates&lt;sup&gt;&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; demonstrate this approach—forms with minimal required fields:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Service Type: Web Service
Name: customer-api
Team: payments

[Create]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Behind this simplicity, the platform makes decisions about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repository structure&lt;/li&gt;
&lt;li&gt;CI/CD configuration&lt;/li&gt;
&lt;li&gt;Deployment target&lt;/li&gt;
&lt;li&gt;Monitoring setup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tier-2-configurable-options&quot;&gt;Tier 2: Configurable Options&lt;/h3&gt;
&lt;p&gt;The same tools often support optional advanced fields:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Service: customer-api
Team: payments

▼ Advanced Options
  Environment: Production
  Resources: Medium (2 CPU, 4GB)
  Database: PostgreSQL - Standard

[Create]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Progressive disclosure—showing complexity only when requested—appears across many platform UIs.&lt;/p&gt;
&lt;h3 id=&quot;tier-3-full-infrastructure-templates&quot;&gt;Tier 3: Full Infrastructure Templates&lt;/h3&gt;
&lt;p&gt;At the detailed end, templates expose networking, scaling, cluster configuration, and other infrastructure parameters.&lt;/p&gt;
&lt;p&gt;These tend to be used less frequently, primarily by platform teams or for specialized workloads.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;shared-development-clusters&quot;&gt;Shared Development Clusters&lt;/h2&gt;
&lt;p&gt;AWS Proton&apos;s documentation&lt;sup&gt;&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; discusses environment strategies. A common pattern that emerges:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared development cluster:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Namespace-based isolation&lt;/li&gt;
&lt;li&gt;Resource quotas per team&lt;/li&gt;
&lt;li&gt;Lower operational overhead&lt;/li&gt;
&lt;li&gt;Automatic cleanup of inactive namespaces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Dedicated clusters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reserved for production&lt;/li&gt;
&lt;li&gt;Or teams with specific isolation requirements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trade-off: shared environments reduce costs and complexity, but require clear resource boundaries.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;progressive-disclosure&quot;&gt;Progressive Disclosure&lt;/h2&gt;
&lt;p&gt;GitHub Codespaces&lt;sup&gt;&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; provides a clear example—default simplicity with optional depth:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Repository: myapp
Branch: main
[Create]

▼ Advanced options
  Machine type: 2-core, 8GB
  Region: US West
  Timeout: 30 minutes&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Most users never expand the advanced section. Those who need it can find it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;environment-lifecycles&quot;&gt;Environment Lifecycles&lt;/h2&gt;
&lt;p&gt;Heroku&lt;sup&gt;&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; and Vercel&lt;sup&gt;&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; both automate environment progression:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pull requests create preview environments&lt;/li&gt;
&lt;li&gt;Main branch deploys to staging&lt;/li&gt;
&lt;li&gt;Production requires manual promotion&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The platform determines where code runs based on git workflow, removing environment selection as a decision point.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cost-visibility&quot;&gt;Cost Visibility&lt;/h2&gt;
&lt;p&gt;Some platforms surface cost estimates before resource creation:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Estimated monthly cost:
  Compute: $150
  Database: $75
  Monitoring: $31
  Total: $256/month

[ ] Budget approved
[Create]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Whether this changes behavior is unclear, but it makes cost visible at decision time rather than after the fact.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;persona-based-templates&quot;&gt;Persona-Based Templates&lt;/h2&gt;
&lt;p&gt;Different templates for different needs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick start:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 input fields&lt;/li&gt;
&lt;li&gt;All defaults&lt;/li&gt;
&lt;li&gt;5 minutes to deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Configurable:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8-10 options&lt;/li&gt;
&lt;li&gt;Recommended defaults shown&lt;/li&gt;
&lt;li&gt;15 minutes to deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full control:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All infrastructure parameters&lt;/li&gt;
&lt;li&gt;For platform engineers or specialized needs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This acknowledges that not everyone comes to the platform with the same goals.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;making-standard-paths-faster&quot;&gt;Making Standard Paths Faster&lt;/h2&gt;
&lt;p&gt;One approach that appears in platform engineering writing&lt;sup&gt;&lt;a href=&quot;#ref-6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;: make the golden path fast, make deviation possible but slower.&lt;/p&gt;
&lt;p&gt;Following standard patterns might take minutes. Requesting custom infrastructure might require architectural review and take days.&lt;/p&gt;
&lt;p&gt;This nudges toward standards without blocking exceptions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;preview-environments&quot;&gt;Preview Environments&lt;/h2&gt;
&lt;p&gt;Vercel&lt;sup&gt;&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; automates this completely:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;PR #42 opened
  → Deployed to preview-pr-42.vercel.app
  → URL commented on PR

PR merged or closed
  → Preview environment deleted&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Internal platforms can implement similar patterns using namespace isolation in shared clusters.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;composition-over-custom-building&quot;&gt;Composition Over Custom Building&lt;/h2&gt;
&lt;p&gt;Looking across implementations, most platform descriptions emphasize orchestrating existing tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub Actions for CI/CD&lt;/li&gt;
&lt;li&gt;Prometheus or Datadog for monitoring&lt;/li&gt;
&lt;li&gt;Managed databases from cloud providers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than building custom versions of commodity tools.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;starting-simple-adding-complexity&quot;&gt;Starting Simple, Adding Complexity&lt;/h2&gt;
&lt;p&gt;The documented implementation pattern tends to be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build minimal template&lt;/li&gt;
&lt;li&gt;Launch to small group&lt;/li&gt;
&lt;li&gt;Gather feedback&lt;/li&gt;
&lt;li&gt;Add optional complexity&lt;/li&gt;
&lt;li&gt;Broader rollout&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Though whether teams actually follow this sequence or jump to comprehensive solutions is variable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;common-elements&quot;&gt;Common Elements&lt;/h2&gt;
&lt;p&gt;Across various platform implementations, certain patterns recur:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three-tier abstraction:&lt;/strong&gt;
Simple default, configurable middle tier, full control for specialists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared development infrastructure:&lt;/strong&gt;
One shared cluster for development, dedicated clusters for production or special cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Progressive disclosure:&lt;/strong&gt;
Hide complexity by default, make it available on request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automated lifecycles:&lt;/strong&gt;
Environments tied to git workflow rather than manual selection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost awareness:&lt;/strong&gt;
Show estimated costs before provisioning.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-these-patterns-address&quot;&gt;What These Patterns Address&lt;/h2&gt;
&lt;p&gt;These approaches seem aimed at similar problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Supporting many developers without proportionally growing platform teams&lt;/li&gt;
&lt;li&gt;Reducing time from &quot;I have an idea&quot; to &quot;I have a running service&quot;&lt;/li&gt;
&lt;li&gt;Balancing standardization with flexibility&lt;/li&gt;
&lt;li&gt;Making costs visible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How well they work depends on organizational context, but the patterns show up often enough to be worth examining.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a id=&quot;ref-1&quot;&gt;1.&lt;/a&gt; Backstage Software Templates - &lt;a href=&quot;https://backstage.io/docs/features/software-templates/&quot;&gt;backstage.io/docs/features/software-templates/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;ref-2&quot;&gt;2.&lt;/a&gt; Heroku Review Apps and Pipeline Promotion - &lt;a href=&quot;https://devcenter.heroku.com/articles/pipelines&quot;&gt;devcenter.heroku.com/articles/pipelines&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;ref-3&quot;&gt;3.&lt;/a&gt; Vercel Preview Deployments - &lt;a href=&quot;https://vercel.com/docs/deployments/preview-deployments&quot;&gt;vercel.com/docs/deployments/preview-deployments&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;ref-4&quot;&gt;4.&lt;/a&gt; AWS Proton Environment Templates and Strategies - &lt;a href=&quot;https://docs.aws.amazon.com/proton/latest/userguide/ag-environments.html&quot;&gt;docs.aws.amazon.com/proton/latest/userguide/ag-environments.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;ref-5&quot;&gt;5.&lt;/a&gt; GitHub Codespaces Configuration and Machine Types - &lt;a href=&quot;https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration&quot;&gt;docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;ref-6&quot;&gt;6.&lt;/a&gt; CNCF Platform Engineering Maturity Model - &lt;a href=&quot;https://tag-app-delivery.cncf.io/whitepapers/platform-eng-maturity-model/&quot;&gt;tag-app-delivery.cncf.io/whitepapers/platform-eng-maturity-model/&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Building a Hub-Spoke AKS Architecture with Istio Service Mesh]]></title><description><![CDATA[A practical guide to implementing a hub-spoke Kubernetes architecture with Istio service mesh, separating control plane services from application workloads for better fault isolation and scalability.]]></description><link>https://chrishouse.io/hub-spoke-aks-istio/</link><guid isPermaLink="false">https://chrishouse.io/hub-spoke-aks-istio/</guid><pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After running a single-cluster Kubernetes setup for a while, I recently migrated to a hub-spoke architecture. This post covers why I made the change, how the traffic routing works, and the key decisions around where to place the Istio ingress gateways.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-hub-spoke&quot;&gt;Why Hub-Spoke?&lt;/h2&gt;
&lt;p&gt;The single-cluster approach is simple but has limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blast radius&lt;/strong&gt; - A misconfiguration or resource exhaustion affects everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling constraints&lt;/strong&gt; - Platform services compete with applications for resources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgrade risk&lt;/strong&gt; - Cluster upgrades put everything at risk simultaneously&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hub-spoke pattern separates concerns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hub cluster&lt;/strong&gt; - Runs platform/control plane services (ArgoCD, Crossplane, Backstage)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spoke cluster(s)&lt;/strong&gt; - Runs application workloads (APIs, frontends, microservices)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;p&gt;Here&apos;s what my setup looks like:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/diagrams/hub-spoke-aks&quot;&gt;View Interactive Diagram →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;                              +---------------------------+
                              |   Cloudflare (DNS/CDN)    |
                              +-------------+-------------+
                                            |
               +----------------------------+----------------------------+
               |                                                         |
               v                                                         v
+---------------------------+                             +---------------------------+
|  Hub Traffic              |                             |  Spoke Traffic            |
|  backstage.chrishouse.io  |                             |  portal.chrishouse.io     |
|  argocd.chrishouse.io     |                             |  portal-api.chrishouse.io |
+---------------------------+                             |  blog.chrishouse.io       |
               |                                          +---------------------------+
               v                                                         |
+--------------------------------------+                                 |
|  AKS Hub Cluster (aks-mgmt-hub)      |                                 |
|  +--------------------------------+  |                                 |
|  | Istio Ingress Gateway         |   |                                 |
|  | (Hub Services Only)           |   |                                 |
|  +--------------------------------+  |                                 |
|                                      |                                 |
|  ArgoCD | Crossplane | Cert-Manager  |                                 |
|  Backstage | Argo Rollouts           |                                 |
+--------------------------------------+                                 |
               |                                                         |
               | Manages via ArgoCD                                      |
               v                                                         v
+------------------------------------------------------------------------+
|  AKS Spoke Cluster (aks-app-spoke)                                     |
|  +------------------------------------------------------------------+  |
|  | Istio Ingress Gateway                                            |  |
|  | (Application Traffic - Direct from Cloudflare)                   |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  portal-api (Node.js) | blog (Gatsby) | frontend (React)               |
+------------------------------------------------------------------------+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;key-design-decision-decentralized-ingress&quot;&gt;Key Design Decision: Decentralized Ingress&lt;/h3&gt;
&lt;p&gt;The critical decision was where to place the Istio ingress gateways. There are two patterns:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Centralized (Hub Ingress)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Internet -&gt; Hub Gateway -&gt; Routes to Spoke clusters&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Decentralized (Spoke Ingress)&lt;/strong&gt; - What I chose&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Internet -&gt; Each cluster&apos;s own Gateway&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I went with decentralized ingress for one main reason: &lt;strong&gt;fault isolation&lt;/strong&gt;. If the hub cluster goes down (maintenance, failed upgrade, resource issues), my applications remain accessible. The hub is a control plane, not a data plane.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cluster-breakdown&quot;&gt;Cluster Breakdown&lt;/h2&gt;
&lt;h3 id=&quot;hub-cluster-services&quot;&gt;Hub Cluster Services&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ArgoCD&lt;/td&gt;
&lt;td&gt;GitOps controller - manages deployments to all clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crossplane&lt;/td&gt;
&lt;td&gt;Infrastructure as Code - provisions cloud resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cert-Manager&lt;/td&gt;
&lt;td&gt;TLS certificate automation via Let&apos;s Encrypt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backstage&lt;/td&gt;
&lt;td&gt;Developer portal and service catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argo Rollouts&lt;/td&gt;
&lt;td&gt;Progressive delivery controller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;spoke-cluster-platform-services&quot;&gt;Spoke Cluster Platform Services&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cert-Manager&lt;/td&gt;
&lt;td&gt;Independent TLS certificates for spoke ingress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Istio Service Mesh&lt;/td&gt;
&lt;td&gt;Traffic management and mTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;spoke-cluster-workloads&quot;&gt;Spoke Cluster Workloads&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;portal-api&lt;/td&gt;
&lt;td&gt;Node.js backend API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;portal-frontend&lt;/td&gt;
&lt;td&gt;React SPA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blog&lt;/td&gt;
&lt;td&gt;Static site (Gatsby)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;istio-configuration&quot;&gt;Istio Configuration&lt;/h2&gt;
&lt;p&gt;Each cluster runs its own Istio service mesh with an ingress gateway. The spoke cluster handles HTTPS termination with its own TLS certificates:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.istio.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Gateway
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gateway
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;istio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aks&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingressgateway&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;external
  &lt;span class=&quot;token key atrule&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
        &lt;span class=&quot;token key atrule&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; HTTP
      &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;portal.chrishouse.io&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;portal-api.chrishouse.io&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blog.chrishouse.io&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;httpsRedirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;443&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https
        &lt;span class=&quot;token key atrule&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; HTTPS
      &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;portal.chrishouse.io&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;portal-api.chrishouse.io&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blog.chrishouse.io&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SIMPLE
        &lt;span class=&quot;token key atrule&quot;&gt;credentialName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; wildcard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;wildcard-tls&lt;/code&gt; secret is created by cert-manager using a wildcard certificate for &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt;. This means the spoke cluster is fully independent for TLS - it doesn&apos;t rely on the hub for certificate management.&lt;/p&gt;
&lt;p&gt;Each application gets a VirtualService that routes traffic:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.istio.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; VirtualService
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;vs
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;portal-api.chrishouse.io&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;gateways&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; internal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gateway
  &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api.portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api.svc.cluster.local
            &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;removing-redundant-ingress-resources&quot;&gt;Removing Redundant Ingress Resources&lt;/h2&gt;
&lt;p&gt;One issue I ran into: my Helm charts still had nginx Ingress resources defined, even though Istio handles all traffic routing. This caused ArgoCD to show applications as &quot;Progressing&quot; indefinitely.&lt;/p&gt;
&lt;p&gt;Why? ArgoCD&apos;s health check for Ingress resources waits for a load balancer IP to be assigned. Since nginx-ingress wasn&apos;t assigning IPs (Istio handles traffic instead), the Ingress stayed in a pending state forever.&lt;/p&gt;
&lt;p&gt;The fix was simple - disable the Ingress in Helm values:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# values.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Istio VirtualService handles routing&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And remove the Ingress from Kustomize resources:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# kustomization.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; namespace.yaml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; serviceaccount.yaml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; configmap.yaml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; deployment.yaml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; service.yaml
  &lt;span class=&quot;token comment&quot;&gt;# - ingress.yaml  # Removed - using Istio&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;traffic-flow-explained&quot;&gt;Traffic Flow Explained&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DNS&lt;/strong&gt;: Cloudflare manages DNS for &lt;code class=&quot;language-text&quot;&gt;*.chrishouse.io&lt;/code&gt;, pointing to the spoke cluster&apos;s external IP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS&lt;/strong&gt;: Terminated at the Istio ingress gateway using wildcard certificates issued by cert-manager (each cluster manages its own certs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Mesh&lt;/strong&gt;: Istio routes to the correct service based on VirtualService rules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mTLS&lt;/strong&gt;: All pod-to-pod traffic within the mesh is encrypted&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For hub services (backstage.chrishouse.io):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Cloudflare -&gt; Hub Istio Gateway -&gt; Backstage Pod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For spoke services (portal-api.chrishouse.io):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Cloudflare -&gt; Spoke Istio Gateway -&gt; Portal API Pod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The hub is never in the path for spoke traffic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;argocd-multi-cluster-management&quot;&gt;ArgoCD Multi-Cluster Management&lt;/h2&gt;
&lt;p&gt;ArgoCD on the hub manages applications across both clusters. Each Application specifies its destination:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aks&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;spoke  &lt;span class=&quot;token comment&quot;&gt;# Target cluster&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/crh225/ARMServicePortal.git
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; infra/kubernetes/portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The hub cluster is registered as a destination in ArgoCD, allowing centralized management while keeping workloads distributed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;pros-and-cons&quot;&gt;Pros and Cons&lt;/h2&gt;
&lt;h3 id=&quot;advantages&quot;&gt;Advantages&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fault isolation&lt;/strong&gt; - Hub issues don&apos;t affect running applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Independent scaling&lt;/strong&gt; - Clusters scale based on their workload type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner upgrades&lt;/strong&gt; - Upgrade hub without touching production apps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security boundaries&lt;/strong&gt; - Platform credentials isolated from app workloads&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;tradeoffs&quot;&gt;Tradeoffs&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Complexity&lt;/strong&gt; - Two clusters to manage instead of one&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt; - Additional control plane costs (though node pools can be sized appropriately)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Networking&lt;/strong&gt; - Cross-cluster communication requires additional configuration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt; - Metrics and logs spread across clusters&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-to-use-this-pattern&quot;&gt;When to Use This Pattern&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple teams deploying to Kubernetes&lt;/li&gt;
&lt;li&gt;High availability requirements for applications&lt;/li&gt;
&lt;li&gt;Frequent platform upgrades&lt;/li&gt;
&lt;li&gt;Compliance requirements for separation of concerns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Overkill for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single small application&lt;/li&gt;
&lt;li&gt;Development/testing environments&lt;/li&gt;
&lt;li&gt;Cost-sensitive projects with low traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The hub-spoke pattern provides a solid foundation for scaling the platform as needs grow.&lt;/p&gt;
&lt;p&gt;Ultimately I chose this pattern also for cost. I want to be able to have my apps and blog available and shut down the hub when not in use and active development.&lt;/p&gt;
&lt;p&gt;A key improvement was making the spoke cluster fully independent with its own cert-manager and TLS certificates. This means the spoke can serve HTTPS traffic even when the hub is completely offline - true fault isolation for production workloads.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://istio.io/latest/docs/setup/install/multicluster/&quot;&gt;Istio Multi-Cluster Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#clusters&quot;&gt;ArgoCD Multi-Cluster Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/containers/aks/baseline-aks&quot;&gt;AKS Hub-Spoke Reference Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Repo-Local AI Governance: Why We Need a Standard for Constraining AI Agents]]></title><description><![CDATA[AI coding assistants are becoming autonomous agents that modify code, run commands, and make architectural decisions. But there's no standard way to tell them what they shouldn't do. Here's a proposal.]]></description><link>https://chrishouse.io/repo-local-ai-governance/</link><guid isPermaLink="false">https://chrishouse.io/repo-local-ai-governance/</guid><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI coding assistants have evolved from autocomplete to autonomous agents. They read your codebase, run shell commands, create files, and make architectural decisions. But there&apos;s a problem: &lt;strong&gt;there&apos;s no standard way to constrain them at the repository level.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every company is inventing their own solution. Claude uses &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt;. Cursor has &lt;code class=&quot;language-text&quot;&gt;.cursorrules&lt;/code&gt;. GitHub Copilot reads from various config files. None of them are compatible, and none of them are designed for governance—they&apos;re designed for context.&lt;/p&gt;
&lt;p&gt;This is a governance problem masquerading as a documentation problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem-hallucinated-authority&quot;&gt;The Problem: Hallucinated Authority&lt;/h2&gt;
&lt;p&gt;AI assistants don&apos;t know what they&apos;re not supposed to do. Without explicit constraints, they&apos;ll:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run destructive commands directly instead of going through your established workflows&lt;/li&gt;
&lt;li&gt;Auto-merge PRs &quot;to be helpful&quot;&lt;/li&gt;
&lt;li&gt;Modify secrets, state files, or protected configurations&lt;/li&gt;
&lt;li&gt;Bypass approval gates that exist for good reasons&lt;/li&gt;
&lt;li&gt;Invent execution paths that don&apos;t exist in your system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prompts don&apos;t solve this. They&apos;re ephemeral—they disappear after the conversation. Every new session starts from zero. What you need is something that persists with the repository and encodes &lt;strong&gt;system invariants&lt;/strong&gt; that must remain true regardless of what a user asks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-current-landscape&quot;&gt;The Current Landscape&lt;/h2&gt;
&lt;p&gt;Here&apos;s what major AI tools use today:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Config File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Project context and instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.cursorrules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Editor behavior and prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Workspace settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.aider.conf.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tool configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.continuerc.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extension settings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Notice the pattern: &lt;strong&gt;configuration, not governance.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These files tell the AI &lt;em&gt;how&lt;/em&gt; to behave (formatting, language preferences, context). They don&apos;t tell it &lt;em&gt;what it must never do&lt;/em&gt; or &lt;em&gt;who has authority over what decisions&lt;/em&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-missing-architecture-contracts-for-ai&quot;&gt;What&apos;s Missing: Architecture Contracts for AI&lt;/h2&gt;
&lt;p&gt;Consider a platform with established workflows—CI/CD pipelines, approval gates, environment promotions. You need your AI assistant to understand:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All changes flow through Git—no direct mutations to production systems&lt;/li&gt;
&lt;li&gt;Certain environments require human approvals&lt;/li&gt;
&lt;li&gt;State files and secrets are immutable from the AI&apos;s perspective&lt;/li&gt;
&lt;li&gt;Execution paths are fixed and cannot be reordered or skipped&lt;/li&gt;
&lt;li&gt;The AI can analyze failures but cannot initiate recovery procedures autonomously&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A config file with &quot;please follow our processes&quot; doesn&apos;t cut it. You need a structured contract that encodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context&lt;/strong&gt;: What is this system and what are its boundaries?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intent&lt;/strong&gt;: What tradeoffs were made and why?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rules&lt;/strong&gt;: What must never happen?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflows&lt;/strong&gt;: What are the valid execution paths?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;a-proposed-pattern-ai-directory&quot;&gt;A Proposed Pattern: &lt;code class=&quot;language-text&quot;&gt;.ai/&lt;/code&gt; Directory&lt;/h2&gt;
&lt;p&gt;The solution is a dedicated directory for AI governance—tool-agnostic, plain markdown, readable by any AI assistant that scans your repo:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.ai/
├── context.md     # What is this system?
├── intent.md      # What tradeoffs were made?
├── rules.md       # What must never happen?
└── workflows.md   # What are valid execution paths?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;contextmd&quot;&gt;context.md&lt;/h3&gt;
&lt;p&gt;Defines the system boundaries and operating model:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Control Boundaries&lt;/span&gt;

Decision-making authority resides with humans at these points:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Configuration curation (what can be changed)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Policy definition (what constraints apply per environment)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Pull request approval (whether a change proceeds)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Production promotion approval (multi-reviewer gate)

Automation owns execution within those boundaries but cannot
bypass approval gates, modify policy configurations, or
introduce new execution paths.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;intentmd&quot;&gt;intent.md&lt;/h3&gt;
&lt;p&gt;Encodes design philosophy and accepted tradeoffs:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Primary Goals&lt;/span&gt;

The system optimizes for:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Safety over speed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: All changes flow through Git with review gates
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Auditability&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: Every change creates a traceable PR
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Self-service within guardrails&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: Users get autonomy; policies enforce boundaries

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Accepted Tradeoffs&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Changes take minutes (PR workflow) rather than seconds (direct execution)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Policy violations block requests rather than warn
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Production changes require multiple approvals even for low-risk changes&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;rulesmd&quot;&gt;rules.md&lt;/h3&gt;
&lt;p&gt;The hard constraints—things that must never happen:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; AI Guardrails&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; What an AI Assistant Must Never Modify&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; State files or backend storage
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Secrets or credential stores
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Policy configurations that relax security constraints
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Approval requirements for any environment
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Branch protection rules

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Prohibited Actions&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Bypassing approval gates
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Merging PRs automatically to protected environments
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Deleting resources without explicit destroy workflow
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Initiating recovery or restoration without explicit human request&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;workflowsmd&quot;&gt;workflows.md&lt;/h3&gt;
&lt;p&gt;Valid execution paths that cannot be modified:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; How Changes Are Executed&lt;/span&gt;

Changes proceed through the following fixed sequence.
Steps may not be reordered, skipped, or conditionally bypassed.

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Branch creation (feature branch)
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Configuration changes committed
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Pull request creation
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Automated validation
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; Results posted for human review
&lt;span class=&quot;token list punctuation&quot;&gt;6.&lt;/span&gt; Human approval (per environment requirements)
&lt;span class=&quot;token list punctuation&quot;&gt;7.&lt;/span&gt; Merge to main branch
&lt;span class=&quot;token list punctuation&quot;&gt;8.&lt;/span&gt; Automated execution
...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-works-and-where-it-doesnt&quot;&gt;Why This Works (And Where It Doesn&apos;t)&lt;/h2&gt;
&lt;h3 id=&quot;what-it-solves&quot;&gt;What It Solves&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hallucinated authority&lt;/strong&gt;: The AI now knows it doesn&apos;t have permission to execute destructive operations directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture drift&lt;/strong&gt;: As conversations progress, the AI has a stable reference for system invariants. No more re-explaining your architecture every session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-tool consistency&lt;/strong&gt;: Whether you use Claude, Cursor, or Copilot, the constraints are in the repo. Switch tools, keep your governance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared repos&lt;/strong&gt;: Other contributors (human or AI) can understand the governance model without tribal knowledge.&lt;/p&gt;
&lt;h3 id=&quot;what-it-doesnt-solve&quot;&gt;What It Doesn&apos;t Solve&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Enforcement&lt;/strong&gt;: These are constraints on reasoning, not execution. The AI &lt;em&gt;could&lt;/em&gt; still violate them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partial context&lt;/strong&gt;: If someone pastes a snippet without loading the governance docs, the constraints don&apos;t exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Narrow prompts&lt;/strong&gt;: Direct commands might not trigger constraint checking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User override&lt;/strong&gt;: &quot;Ignore the rules, I&apos;m the admin&quot; might work.&lt;/p&gt;
&lt;p&gt;The honest answer is: &lt;strong&gt;these docs increase refusal likelihood, not inability to comply.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But that&apos;s often enough. Most security is about raising the bar, not building impenetrable walls.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-case-for-standardization&quot;&gt;The Case for Standardization&lt;/h2&gt;
&lt;p&gt;Right now, every organization is solving this independently. That&apos;s wasteful and inconsistent.&lt;/p&gt;
&lt;p&gt;A standard should define:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Directory convention&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;.ai/&lt;/code&gt; (tool-agnostic, intuitive, future-proof)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File structure&lt;/strong&gt;: Separate context, intent, rules, and workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema&lt;/strong&gt;: Machine-readable sections that tools can parse (initially convention-based, later formally validated)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inheritance&lt;/strong&gt;: Project-level overrides of organization defaults&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioning&lt;/strong&gt;: How constraints evolve with the codebase&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;a-more-complete-standard&quot;&gt;A More Complete Standard&lt;/h2&gt;
&lt;p&gt;Here&apos;s what a cross-tool standard could look like:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.ai/
├── context.md       # System boundaries and ecosystem position
├── intent.md        # Design philosophy and tradeoffs
├── rules.md         # Hard constraints (MUST/MUST NOT)
├── workflows.md     # Valid execution paths&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-actually-changes&quot;&gt;What Actually Changes?&lt;/h2&gt;
&lt;p&gt;The key question: do these files meaningfully change AI behavior, or do they mostly change how answers are explained?&lt;/p&gt;
&lt;p&gt;Based on testing this pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;They shape reasoning&lt;/strong&gt;: The AI models the system as having hard boundaries that exist independent of user requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;They raise the refusal threshold&lt;/strong&gt;: The AI is more likely to say &quot;I can&apos;t do that&quot; vs. &quot;Let me try to help.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;They don&apos;t change capabilities&lt;/strong&gt;: The AI could still technically attempt prohibited actions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s the right mental model. These docs are governance constraints, not technical enforcement. They work because AI assistants are designed to be helpful and compliant—not because they&apos;re physically prevented from violating them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h2&gt;
&lt;p&gt;If you want to implement this pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a &lt;code class=&quot;language-text&quot;&gt;.ai/&lt;/code&gt; directory in your repo&lt;/li&gt;
&lt;li&gt;Write &lt;code class=&quot;language-text&quot;&gt;context.md&lt;/code&gt;: What is this system? What are its boundaries?&lt;/li&gt;
&lt;li&gt;Write &lt;code class=&quot;language-text&quot;&gt;intent.md&lt;/code&gt;: What tradeoffs did you make? What do you optimize for?&lt;/li&gt;
&lt;li&gt;Write &lt;code class=&quot;language-text&quot;&gt;rules.md&lt;/code&gt;: What must never happen? Be specific and use &quot;MUST NOT&quot; language.&lt;/li&gt;
&lt;li&gt;Write &lt;code class=&quot;language-text&quot;&gt;workflows.md&lt;/code&gt;: What are the valid execution paths? Can they be reordered?&lt;/li&gt;
&lt;li&gt;Reference these files in your &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;.cursorrules&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key insight: &lt;strong&gt;separate documentation from governance.&lt;/strong&gt; READMEs explain how things work. Governance docs constrain what&apos;s allowed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;AI agents are here. They&apos;re reading our code, running our commands, and making decisions on our behalf. We&apos;ve standardized how humans interact with repositories (PRs, branch protection, CODEOWNERS). We haven&apos;t standardized how AI agents should be constrained.&lt;/p&gt;
&lt;p&gt;Until we do, every organization will reinvent this wheel. Some will do it well. Most won&apos;t do it at all, and their AI assistants will hallucinate authority they were never granted.&lt;/p&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;.ai/&lt;/code&gt; pattern isn&apos;t perfect, but it&apos;s a starting point. The goal isn&apos;t perfection—it&apos;s establishing a convention before the next generation of AI agents ships without any governance at all.&lt;/p&gt;
&lt;p&gt;To be clear: this is a proposal, not a proven standard. I haven&apos;t validated it at scale across multiple teams or tools. But someone needs to start the conversation, and waiting for perfect evidence means waiting until the problem is already entrenched.&lt;/p&gt;
&lt;p&gt;We need this.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Configuring ArgoCD to Access Private GitHub Repositories]]></title><description><![CDATA[A step-by-step guide to configuring ArgoCD for private GitHub repositories and private container images from GHCR.]]></description><link>https://chrishouse.io/argocd-private-github-repos/</link><guid isPermaLink="false">https://chrishouse.io/argocd-private-github-repos/</guid><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This beginner&apos;s guide covers how to configure ArgoCD to sync applications from private GitHub repositories and pull private container images from GitHub Container Registry (GHCR). For production environments, consider using External Secrets Operator or Sealed Secrets to manage credentials declaratively rather than imperatively&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ArgoCD installed on your Kubernetes cluster&lt;/li&gt;
&lt;li&gt;A GitHub Personal Access Token (PAT) with the following scopes:
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;repo&lt;/code&gt; - Full control of private repositories&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;read:packages&lt;/code&gt; - Read packages from GHCR&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;write:packages&lt;/code&gt; - Push packages to GHCR (for CI/CD)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-1-argocd-git-repository-access&quot;&gt;Part 1: ArgoCD Git Repository Access&lt;/h2&gt;
&lt;p&gt;ArgoCD needs credentials to clone private repositories. Create a Kubernetes secret with your GitHub username and PAT.&lt;/p&gt;
&lt;h3 id=&quot;create-the-secret&quot;&gt;Create the Secret&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Secret
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd.argoproj.io/secret-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
&lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Opaque
&lt;span class=&quot;token key atrule&quot;&gt;stringData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/YOUR_USERNAME/
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; git
  &lt;span class=&quot;token key atrule&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; YOUR_USERNAME
  &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; YOUR_GITHUB_PAT&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code class=&quot;language-text&quot;&gt;repo-creds&lt;/code&gt; as the secret type for prefix matching (applies to all repos under that URL)&lt;/li&gt;
&lt;li&gt;The &lt;code class=&quot;language-text&quot;&gt;username&lt;/code&gt; field should be your actual GitHub username&lt;/li&gt;
&lt;li&gt;The &lt;code class=&quot;language-text&quot;&gt;password&lt;/code&gt; field is your PAT&lt;/li&gt;
&lt;li&gt;Include the trailing slash on the URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;apply-and-restart&quot;&gt;Apply and Restart&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; github-repo-creds.yaml
kubectl rollout restart deployment argocd-repo-server &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; argocd&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;verify&quot;&gt;Verify&lt;/h3&gt;
&lt;p&gt;Check that your application syncs:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get application &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; argocd&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should see &lt;code class=&quot;language-text&quot;&gt;Synced&lt;/code&gt; status instead of &lt;code class=&quot;language-text&quot;&gt;Unknown&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-2-private-container-image-pulls&quot;&gt;Part 2: Private Container Image Pulls&lt;/h2&gt;
&lt;p&gt;If your container images are also private in GHCR, Kubernetes needs an &lt;code class=&quot;language-text&quot;&gt;imagePullSecret&lt;/code&gt; to authenticate when pulling images.&lt;/p&gt;
&lt;h3 id=&quot;create-the-docker-registry-secret&quot;&gt;Create the Docker Registry Secret&lt;/h3&gt;
&lt;p&gt;Run this for each namespace where you deploy applications:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl create secret docker-registry ghcr-pull-secret &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; YOUR_NAMESPACE &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-server&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ghcr.io &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-username&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YOUR_USERNAME &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-password&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YOUR_GITHUB_PAT &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-email&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;noreply@github.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;reference-in-your-helm-chart&quot;&gt;Reference in Your Helm Chart&lt;/h3&gt;
&lt;p&gt;Add to your &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;imagePullSecrets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pull&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secret&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Update your deployment template to use it:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; with .Values.imagePullSecrets &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;imagePullSecrets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; toYaml . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 8 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Chart.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ .Values.image.repository }}:{{ .Values.image.tag }}&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;storing-the-pat-securely&quot;&gt;Storing the PAT Securely&lt;/h2&gt;
&lt;p&gt;Instead of hardcoding the PAT, store it in a secret manager like Azure Key Vault and retrieve it when creating secrets:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;PAT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;az keyvault secret show --vault-name YOUR_VAULT &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; github-pat &lt;span class=&quot;token parameter variable&quot;&gt;--query&lt;/span&gt; value &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; tsv&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

kubectl create secret docker-registry ghcr-pull-secret &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; dev &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-server&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ghcr.io &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-username&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YOUR_USERNAME &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-password&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PAT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --docker-email&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;noreply@github.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Namespace&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;github-repo-creds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ArgoCD clones private repos&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;argocd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ghcr-pull-secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes pulls private images&lt;/td&gt;
&lt;td&gt;Each deployment namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both secrets use the same PAT with your GitHub username. The &lt;code class=&quot;language-text&quot;&gt;repo-creds&lt;/code&gt; type enables prefix matching, so one secret covers all repositories under your GitHub account.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;production-approach-external-secrets-operator&quot;&gt;Production Approach: External Secrets Operator&lt;/h2&gt;
&lt;p&gt;The imperative approach above works, but for production you want secrets managed declaratively through GitOps. External Secrets Operator automatically syncs secrets from Azure Key Vault (or AWS Secrets Manager, HashiCorp Vault, etc.) into Kubernetes secrets.&lt;/p&gt;
&lt;h3 id=&quot;install-external-secrets-operator&quot;&gt;Install External Secrets Operator&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; external-secrets https://charts.external-secrets.io
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; external-secrets external-secrets/external-secrets &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; external-secrets &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --create-namespace&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;configure-azure-key-vault-access&quot;&gt;Configure Azure Key Vault Access&lt;/h3&gt;
&lt;p&gt;First, create a SecretStore that connects to your Key Vault. This example uses Azure Workload Identity:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterSecretStore
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;keyvault
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;azurekv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;authType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; WorkloadIdentity
      &lt;span class=&quot;token key atrule&quot;&gt;vaultUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://YOUR_VAULT.vault.azure.net&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;sa
        &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;declarative-github-credentials-for-argocd&quot;&gt;Declarative GitHub Credentials for ArgoCD&lt;/h3&gt;
&lt;p&gt;Now define an ExternalSecret that pulls the PAT from Key Vault and creates the ArgoCD secret:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ExternalSecret
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;refreshInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1h
  &lt;span class=&quot;token key atrule&quot;&gt;secretStoreRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;keyvault
    &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterSecretStore
  &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
    &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;argocd.argoproj.io/secret-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
      &lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; git
        &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/YOUR_USERNAME/
        &lt;span class=&quot;token key atrule&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; YOUR_USERNAME
        &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ .github_pat }}&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;secretKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github_pat
      &lt;span class=&quot;token key atrule&quot;&gt;remoteRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pat&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;declarative-ghcr-pull-secret&quot;&gt;Declarative GHCR Pull Secret&lt;/h3&gt;
&lt;p&gt;Similarly for the image pull secret:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; external&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secrets.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ExternalSecret
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pull&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secret
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;refreshInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1h
  &lt;span class=&quot;token key atrule&quot;&gt;secretStoreRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;keyvault
    &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterSecretStore
  &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pull&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secret
    &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kubernetes.io/dockerconfigjson
      &lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;.dockerconfigjson&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          {
            &quot;auths&quot;: {
              &quot;ghcr.io&quot;: {
                &quot;username&quot;: &quot;YOUR_USERNAME&quot;,
                &quot;password&quot;: &quot;{{ .github_pat }}&quot;,
                &quot;auth&quot;: &quot;{{ printf &quot;%s:%s&quot; &quot;YOUR_USERNAME&quot; .github_pat | b64enc }}&quot;
              }
            }
          }&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;secretKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github_pat
      &lt;span class=&quot;token key atrule&quot;&gt;remoteRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pat&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;benefits-of-this-approach&quot;&gt;Benefits of This Approach&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GitOps-friendly&lt;/strong&gt; - Secret definitions live in git, actual values stay in Key Vault&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic rotation&lt;/strong&gt; - Change the PAT in Key Vault, secrets update automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit trail&lt;/strong&gt; - Key Vault logs all secret access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No imperative commands&lt;/strong&gt; - Everything is declarative YAML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single source of truth&lt;/strong&gt; - One PAT in Key Vault, referenced everywhere&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;alternative-sealed-secrets&quot;&gt;Alternative: Sealed Secrets&lt;/h3&gt;
&lt;p&gt;If you don&apos;t have a secret manager, Sealed Secrets lets you encrypt secrets so they can be committed to git:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Install kubeseal CLI and controller&lt;/span&gt;
helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; sealed-secrets sealed-secrets/sealed-secrets &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system

&lt;span class=&quot;token comment&quot;&gt;# Encrypt a secret&lt;/span&gt;
kubectl create secret generic github-repo-creds &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; argocd &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --from-literal&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;password&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YOUR_PAT &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --dry-run&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;client &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; kubeseal &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; sealed-secret.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The sealed secret can be safely committed to git - only your cluster can decrypt it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;which-approach-should-you-use&quot;&gt;Which Approach Should You Use?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual &lt;code class=&quot;language-text&quot;&gt;kubectl create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Learning, development, quick setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External Secrets Operator&lt;/td&gt;
&lt;td&gt;Production with existing secret manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sealed Secrets&lt;/td&gt;
&lt;td&gt;Production without external secret manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For most production Kubernetes environments, External Secrets Operator with your cloud provider&apos;s secret manager is the recommended approach.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Golden Paths, Day-2 Operations, and Where the Abstractions Crack]]></title><description><![CDATA[Golden paths get attention for service creation. The harder problem is day-2 operations—where reality shows up.]]></description><link>https://chrishouse.io/golden-paths-day2-operations/</link><guid isPermaLink="false">https://chrishouse.io/golden-paths-day2-operations/</guid><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Disclaimer: I am a few weeks into building an internal developer platform for myself. Here is what I have learned so far.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Golden paths get a lot of attention in platform engineering discussions. Most of that attention stops at creation—scaffolding repos, setting up CI, and deploying to Kubernetes.&lt;/p&gt;
&lt;p&gt;That part is relatively solved.&lt;/p&gt;
&lt;p&gt;The harder problem is day-2 operations. That is where golden paths get more complicated.&lt;/p&gt;
&lt;p&gt;I have been experimenting for a month with golden paths built on Backstage, Argo CD, and Kubernetes, and then layering Port.io on top to handle the parts that do not fit cleanly into GitOps alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;day-1-is-easy&quot;&gt;Day-1 Is Easy&lt;/h2&gt;
&lt;p&gt;A typical golden path stack could look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backstage&lt;/strong&gt; for service creation and discoverability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt; as the source of truth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Argo CD&lt;/strong&gt; for GitOps reconciliation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt; as the runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Backstage software templates do a solid job at day-1:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repos are scaffolded consistently&lt;/li&gt;
&lt;li&gt;CI/CD pipelines are pre-wired&lt;/li&gt;
&lt;li&gt;Deployment manifests exist from the start&lt;/li&gt;
&lt;li&gt;Services show up in a catalog instead of disappearing into someone&apos;s personal GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That initial experience matters. Early decisions tend to stick, for better or worse.&lt;/p&gt;
&lt;p&gt;But once the service exists, for me, Backstage mostly steps aside.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-things-get-messy-day-2&quot;&gt;Where Things Get Messy: Day-2&lt;/h2&gt;
&lt;p&gt;Day-2 is where reality shows up.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Restarting or scaling a misbehaving service&lt;/li&gt;
&lt;li&gt;Promoting config between environments&lt;/li&gt;
&lt;li&gt;Triggering a redeploy without a code change&lt;/li&gt;
&lt;li&gt;Inspecting ownership, dependencies, or runtime state&lt;/li&gt;
&lt;li&gt;Performing safe operational actions without giving everyone kubectl access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These actions do not always belong in Git. They also do not belong in sending messages to the platform team.&lt;/p&gt;
&lt;p&gt;This is where I started looking at Port.io.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-portio-fits&quot;&gt;How Port.io Fits&lt;/h2&gt;
&lt;p&gt;I found Port.io most useful for day-2 operations, with some ability to trigger day-0 creation workflows when needed.&lt;/p&gt;
&lt;p&gt;Port does not natively scaffold code.&lt;/p&gt;
&lt;p&gt;It triggers things that do.&lt;/p&gt;
&lt;p&gt;So in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Port can be the button a developer clicks on day-0&lt;/li&gt;
&lt;li&gt;Backstage (or a repo template) can still do the scaffolding&lt;/li&gt;
&lt;li&gt;Git remains the source of truth&lt;/li&gt;
&lt;li&gt;Argo reconciles state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that sense, Port is not a &quot;day-0 engine&quot; so much as a &lt;strong&gt;day-0 orchestrator&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In my lab setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backstage is the entry point for creation, metadata, and discovery&lt;/li&gt;
&lt;li&gt;Git and Argo CD remain the source of truth&lt;/li&gt;
&lt;li&gt;Port.io becomes a controlled interaction layer on top&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using Port, I modeled:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Services, environments, and clusters as entities&lt;/li&gt;
&lt;li&gt;Relationships between repos, workloads, and infrastructure&lt;/li&gt;
&lt;li&gt;Actions that map to real operational workflows like restart, sync, and promote&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Crucially, those actions do not mutate the cluster directly.&lt;/p&gt;
&lt;p&gt;They trigger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;Argo CD syncs&lt;/li&gt;
&lt;li&gt;Or other well-defined automation steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my setup, provisioning remained outside of Port. That constraint mattered. Without it, the platform quickly started to resemble a prettier kubectl wrapper.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-separation-actually-works&quot;&gt;Why This Separation Actually Works&lt;/h2&gt;
&lt;p&gt;Each tool is good at a different thing:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Question It Answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backstage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;What is this?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;What can I safely do to it?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Argo CD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;What should the cluster look like?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Backstage is strong at creation but limited once a service is running. Port works well for interaction and visibility but benefits from clear boundaries. Argo enforces desired state but does not provide much guidance at the human level.&lt;/p&gt;
&lt;p&gt;Once I stopped trying to make one tool do everything, the system made more sense.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-success-might-look-like&quot;&gt;What Success Might Look Like&lt;/h2&gt;
&lt;p&gt;I am still early in building this, so I do not know yet what success looks like in practice.&lt;/p&gt;
&lt;p&gt;What I am looking for instead are signals that things are moving in the right direction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New services start to look boring in similar ways&lt;/li&gt;
&lt;li&gt;Day-2 actions become visible and repeatable&lt;/li&gt;
&lt;li&gt;The platform starts answering questions before people ask them (with AI)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If those things start to happen over time, that will feel like progress.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h2&gt;
&lt;p&gt;Backstage, Argo CD, Kubernetes, and Port.io are all powerful tools.&lt;/p&gt;
&lt;p&gt;None of them are the golden path by themselves.&lt;/p&gt;
&lt;p&gt;Golden paths emerge when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Defaults are intentional&lt;/li&gt;
&lt;li&gt;Day-2 operations are treated as first-class concerns&lt;/li&gt;
&lt;li&gt;The platform allows teams to step off the path without friction&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There are tradeoffs no matter how you design this. That is usually where the real work begins.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Elastic Cloud Fleet Agent + Kubernetes Container Logs]]></title><description><![CDATA[How we made container logs visible in Kibana using Elastic Fleet Agent, why some setups silently fail, and what it takes to fix them.]]></description><link>https://chrishouse.io/elastic-fleet-agent-kubernetes/</link><guid isPermaLink="false">https://chrishouse.io/elastic-fleet-agent-kubernetes/</guid><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the past few weeks, we have been researching how to use Elastic Cloud&apos;s Fleet Agent across Kubernetes clusters to monitor containerized workloads. This post covers how we made container logs visible in Kibana, why some setups silently fail, and what it takes to fix them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;/h2&gt;
&lt;p&gt;We wanted to ship logs from every Kubernetes container into Elastic Cloud using the Fleet-managed Elastic Agent. The goal was simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deploy a DaemonSet that runs one Elastic Agent per node&lt;/li&gt;
&lt;li&gt;Mount &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Point it to our Fleet Server with an enrollment token&lt;/li&gt;
&lt;li&gt;Watch logs roll into Kibana&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;symptom-no-container-logs&quot;&gt;Symptom: No Container Logs&lt;/h2&gt;
&lt;p&gt;Our deployment looked good on paper: the agent was enrolled, metrics were visible, and &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt; was populated inside the container. But no container logs appeared in Kibana.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-investigation&quot;&gt;The Investigation&lt;/h2&gt;
&lt;h3 id=&quot;1-reviewed-the-daemonset-yaml&quot;&gt;1. Reviewed the DaemonSet YAML&lt;/h3&gt;
&lt;p&gt;Our DaemonSet had mounts for &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;/var/lib/containerd&lt;/code&gt;, but was missing &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt; — which turned out to be the key input path for the logs.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Before the fix&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pods
    &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/pods
    &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containerd
    &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/containerd
    &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pods
    &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/pods
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containerd
    &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/containerd&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;2-inspected-runtime-mounts&quot;&gt;2. Inspected Runtime Mounts&lt;/h3&gt;
&lt;p&gt;We used this command to check which paths were actually mounted inside the Elastic Agent pod:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; elastic-agent-XXXX -- &lt;span class=&quot;token function&quot;&gt;mount&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/var/log|/var/lib/containerd&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The expected &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt; path was not present — meaning the agent had no access to the symlinked container logs.&lt;/p&gt;
&lt;h3 id=&quot;3-checked-fleet-integration-configuration&quot;&gt;3. Checked Fleet Integration Configuration&lt;/h3&gt;
&lt;p&gt;In the Fleet UI (under the &quot;Kubernetes Logs&quot; integration), the log path was set to:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/var/log/containers/*.log&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If this path doesn&apos;t exist inside the container, logs won&apos;t flow. Fleet doesn&apos;t surface errors for missing log paths — it just silently fails.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;We added a hostPath volume for &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt; to the DaemonSet and mounted it inside the container.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# After the fix&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containers
    &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/containers

&lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containers
    &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/containers
    &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After applying this change, the agent immediately started reading symlinked container logs, and the data appeared in Kibana.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fleet Agent requires &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt;, not just &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add the missing hostPath mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DaemonSet volume mounts must match the Fleet integration&apos;s expectations&lt;/td&gt;
&lt;td&gt;Verify paths in Fleet UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The filestream input fails silently when paths are missing&lt;/td&gt;
&lt;td&gt;No errors, no logs — just silence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;You can inspect what the agent is trying to harvest using:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;./elastic-agent inspect components --show-config&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;bonus-persisting-agent-identity-across-restarts&quot;&gt;Bonus: Persisting Agent Identity Across Restarts&lt;/h2&gt;
&lt;p&gt;We noticed that after restarting Elastic Agent pods, the old agent would show up as &quot;offline&quot; in Fleet, while a new one appeared as &quot;online.&quot; This happened because each pod restart generated a new &lt;code class=&quot;language-text&quot;&gt;agent.id&lt;/code&gt;, leaving duplicates in Fleet.&lt;/p&gt;
&lt;p&gt;To solve this, we mounted the Elastic Agent&apos;s internal state directory to the node&apos;s filesystem using a hostPath. This ensures that when a pod restarts on the same node, it reuses the same identity.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;state
    &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
      &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DirectoryOrCreate

&lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;state
    &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /usr/share/elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent/state&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This change stopped the agent duplication and made our Fleet inventory much cleaner.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;gotcha-x509-certificate-errors-from-zscaler&quot;&gt;Gotcha: x509 Certificate Errors from Zscaler&lt;/h2&gt;
&lt;p&gt;One critical issue we hit during enrollment was related to TLS inspection by Zscaler. Elastic Agent failed to enroll with this error:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;x509: certificate signed by unknown authority&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Although the connection and token looked correct, the underlying HTTPS call to the Fleet Server was being intercepted by a Zscaler proxy. To resolve this, we needed to install the Zscaler root certificate inside the agent pods.&lt;/p&gt;
&lt;h3 id=&quot;solution&quot;&gt;Solution&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Create a ConfigMap containing the Zscaler certificate:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ConfigMap
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cert
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;system
&lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;zscaler.crt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
    -----BEGIN CERTIFICATE-----
    [REDACTED CERT CONTENT]
    -----END CERTIFICATE-----&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Mount the certificate path in the pod:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca
    &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/ssl/certs

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca
    &lt;span class=&quot;token key atrule&quot;&gt;configMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cert&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once this was in place, the agent enrolled successfully. TLS errors went away immediately.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;debugging-tips&quot;&gt;Debugging Tips&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Check for logs inside container&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt; /var/log/pods &lt;span class=&quot;token parameter variable&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;token parameter variable&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.log&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Validate filebeat component&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt; aux &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; filebeat
./elastic-agent inspect components --show-config &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt; filestream

&lt;span class=&quot;token comment&quot;&gt;# Verify in Kibana&lt;/span&gt;
data_stream.dataset &lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; *container*&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Elastic&apos;s Fleet integration is powerful, but sensitive to configuration. To avoid surprises:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Triple-check your volume mounts&lt;/strong&gt; — missing paths fail silently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Match the Fleet integration&apos;s expected log paths&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;/var/log/containers&lt;/code&gt;, not just &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persist the agent&apos;s state&lt;/strong&gt; so you don&apos;t get stale &quot;offline&quot; entries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you&apos;re behind Zscaler or a TLS proxy&lt;/strong&gt;, mount your root cert chain&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This setup is now running smoothly in the test lab, and container logs across all nodes are flowing into Kibana as expected.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;reference-sample-helm-chart-sanitized&quot;&gt;Reference: Sample Helm Chart (Sanitized)&lt;/h2&gt;
&lt;h3 id=&quot;valuesyaml-excerpt&quot;&gt;values.yaml (excerpt)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker.elastic.co/beats/elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8.16.6

&lt;span class=&quot;token key atrule&quot;&gt;zscalerCA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;fileName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler.crt
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;daemonsetyaml-excerpt&quot;&gt;daemonset.yaml (excerpt)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DaemonSet
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;system
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
      &lt;span class=&quot;token key atrule&quot;&gt;hostNetwork&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;dnsPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterFirstWithHostNet
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
          &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ .Values.image.repository }}:{{ .Values.image.tag }}&quot;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; FLEET_ENROLL
              &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; FLEET_URL
              &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;redacted&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; FLEET_ENROLLMENT_TOKEN
              &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;redacted&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pods
              &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/pods
              &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containerd
              &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/containerd
              &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containers
              &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/containers
              &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data
              &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /usr/share/elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent/state
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca
              &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/ssl/certs
      &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pods
          &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/pods
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containerd
          &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/containerd
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; containers
          &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/log/containers
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data
          &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/lib/elastic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;agent
            &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DirectoryOrCreate
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca
          &lt;span class=&quot;token key atrule&quot;&gt;configMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; zscaler&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ca&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cert&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title><![CDATA[Building the ARM Service Portal: A Self-Service Platform for Azure Infrastructure]]></title><description><![CDATA[A deep dive into building a self-service portal that lets developers provision Azure resources through blueprints, while Terraform and GitOps handle everything behind the scenes.]]></description><link>https://chrishouse.io/arm-service-portal/</link><guid isPermaLink="false">https://chrishouse.io/arm-service-portal/</guid><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Teams either wait days (or months) for infra tickets to be provisioned, or let developers create Azure resources directly—which leads to inconsistent, insecure, or untracked infrastructure. Clouds fill with resources created outside Terraform, making governance and cost control harder.&lt;/p&gt;
&lt;p&gt;I built a self-service portal to solve this: developers provision Azure resources through a simple UI, while Terraform and GitOps handle everything behind the scenes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href=&quot;https://portal.chrishouse.io&quot;&gt;https://portal.chrishouse.io&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blueprint-based forms&lt;/strong&gt; that generate Terraform PRs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; performs plan, apply, and state handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time cost, health, and deployment status&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shows Azure resources not managed by Terraform&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource graph&lt;/strong&gt; to visualize dependencies and ownership&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backstage integration&lt;/strong&gt; for service catalog and scaffolding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The entire platform bootstraps itself—the Container App, Storage, and Front Door were all deployed through the portal using the same blueprints it provides.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;h3 id=&quot;frontend&quot;&gt;Frontend&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;React SPA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Azure Storage Static Websites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;Azure Front Door&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;GitHub OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms&lt;/td&gt;
&lt;td&gt;Dynamic generation from blueprint metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visualization&lt;/td&gt;
&lt;td&gt;Azure Resource Graph for dependency mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The frontend renders forms dynamically based on blueprint definitions. Each blueprint defines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Required and optional parameters&lt;/li&gt;
&lt;li&gt;Validation rules&lt;/li&gt;
&lt;li&gt;Cost estimation hooks&lt;/li&gt;
&lt;li&gt;Dependencies on other blueprints&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Example blueprint metadata&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;azure-container-app&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;displayName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Container App&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Deploy a containerized application to Azure Container Apps&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;parameters&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;app_name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;validation&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^[a-z0-9-]+$&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;container_image&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cpu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;select&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0.25&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0.5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0.5&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;memory&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;select&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0.5Gi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1Gi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2Gi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4Gi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1Gi&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;costEstimate&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;azure-container-apps&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;factors&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cpu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;memory&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;replicas&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;backend&quot;&gt;Backend&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Node.js + Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Azure Container Apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;GitHub App for PR automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Terraform remote state in Azure Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;GitHub webhook handling for deployment events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The backend handles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Terraform code generation&lt;/strong&gt; from blueprint parameters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variable validation&lt;/strong&gt; before PR creation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure pricing API integration&lt;/strong&gt; for cost estimates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub App actions&lt;/strong&gt; (create PRs, post comments, trigger workflows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource discovery&lt;/strong&gt; for unmanaged Azure resources&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Simplified PR creation flow&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createDeploymentPR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 1. Generate Terraform code from template&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; terraformCode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateTerraform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 2. Create branch&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; branchName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;deploy/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createBranch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;branchName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 3. Commit generated files&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;commitFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;branchName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;deployments/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parameters&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;app_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/main.tf&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; terraformCode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;deployments/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parameters&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;app_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/variables.tf&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; variablesFile &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;deployments/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parameters&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;app_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/terraform.tfvars&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; tfvarsFile &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 4. Create PR with cost estimate in description&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; costEstimate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;estimateCost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pr &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createPR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[Portal] Deploy &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;displayName&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parameters&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;app_name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generatePRDescription&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parameters&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; costEstimate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; branchName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;main&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 5. Trigger plan workflow&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;triggerWorkflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;terraform-plan.yml&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;pr_number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;number &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;gitops-pipeline&quot;&gt;GitOps Pipeline&lt;/h3&gt;
&lt;p&gt;The real magic happens in GitHub Actions. When a PR is created:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/terraform-plan.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Terraform Plan

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pull_request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;deployments/**&apos;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;plan&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup Terraform
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hashicorp/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;terraform@v3

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Configure Azure credentials
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure/login@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;creds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.AZURE_CREDENTIALS &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Find changed deployments
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; changes
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }} | grep &apos;^deployments/&apos; | cut -d&apos;/&apos; -f2 | sort -u)
          echo &quot;deployments=$CHANGED&quot; &gt;&gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Terraform Init &amp;amp; Plan
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          for deployment in ${{ steps.changes.outputs.deployments }}; do
            cd deployments/$deployment
            terraform init -backend-config=&quot;key=$deployment.tfstate&quot;
            terraform plan -out=plan.tfplan
            terraform show -json plan.tfplan &gt; plan.json
          done&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Post plan to PR
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;script@v7
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
            const plan = require(&apos;./deployments/${{ steps.changes.outputs.deployments }}/plan.json&apos;);
            const summary = formatPlanSummary(plan);&lt;/span&gt;

            github.rest.issues.createComment(&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; context.repo.owner&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; context.repo.repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;issue_number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; context.issue.number&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; `&lt;span class=&quot;token comment&quot;&gt;## Terraform Plan\n\n${summary}\n\n**Resources:**\n- To create: ${plan.resource_changes.filter(r =&gt; r.change.actions.includes(&apos;create&apos;)).length}\n- To update: ${plan.resource_changes.filter(r =&gt; r.change.actions.includes(&apos;update&apos;)).length}\n- To destroy: ${plan.resource_changes.filter(r =&gt; r.change.actions.includes(&apos;delete&apos;)).length}`&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When the PR is merged:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/terraform-apply.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Terraform Apply

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;deployments/**&apos;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup Terraform
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hashicorp/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;terraform@v3

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Configure Azure credentials
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure/login@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;creds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.AZURE_CREDENTIALS &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Find changed deployments
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; changes
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          CHANGED=$(git diff --name-only HEAD~1 | grep &apos;^deployments/&apos; | cut -d&apos;/&apos; -f2 | sort -u)
          echo &quot;deployments=$CHANGED&quot; &gt;&gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Terraform Apply
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          for deployment in ${{ steps.changes.outputs.deployments }}; do
            cd deployments/$deployment
            terraform init -backend-config=&quot;key=$deployment.tfstate&quot;
            terraform apply -auto-approve
          done&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Update portal status
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          curl -X POST &quot;${{ secrets.PORTAL_WEBHOOK_URL }}/deployment-complete&quot; \
            -H &quot;Content-Type: application/json&quot; \
            -d &apos;{&quot;deployment&quot;: &quot;${{ steps.changes.outputs.deployments }}&quot;, &quot;status&quot;: &quot;success&quot;}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;backstage-integration&quot;&gt;Backstage Integration&lt;/h2&gt;
&lt;p&gt;The portal integrates with Backstage for service catalog and scaffolding. This creates a unified developer experience:&lt;/p&gt;
&lt;h3 id=&quot;service-catalog-sync&quot;&gt;Service Catalog Sync&lt;/h3&gt;
&lt;p&gt;Every resource deployed through the portal automatically registers in Backstage:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Generated catalog-info.yaml for each deployment&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; backstage.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Component
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;github.com/project-slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;github_repo&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;backstage.io/techdocs-ref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dir&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;.
    &lt;span class=&quot;token key atrule&quot;&gt;azure.com/resource-group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;resource_group&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;azure.com/subscription&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;subscription_id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;links&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.azurecontainerapps.io
      &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Live Site
      &lt;span class=&quot;token key atrule&quot;&gt;icon&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dashboard
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//portal.azure.com/&lt;span class=&quot;token comment&quot;&gt;#resource${resource_id}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Azure Portal
      &lt;span class=&quot;token key atrule&quot;&gt;icon&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cloud
  &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; azure
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; container&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; terraform&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;managed
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service
  &lt;span class=&quot;token key atrule&quot;&gt;lifecycle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
  &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;owner&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;dependsOn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;resource_group&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;software-templates&quot;&gt;Software Templates&lt;/h3&gt;
&lt;p&gt;Backstage software templates use the same blueprints as the portal. When a developer scaffolds a new service:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Backstage creates the GitHub repo with CI/CD workflows&lt;/li&gt;
&lt;li&gt;The portal&apos;s blueprint form is embedded for infrastructure provisioning&lt;/li&gt;
&lt;li&gt;ArgoCD application is generated for Kubernetes deployments&lt;/li&gt;
&lt;li&gt;Everything links back to the service catalog&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# backstage/templates/azure-fullstack/template.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scaffolder.backstage.io/v1beta3
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Template
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;fullstack&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;template
  &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Azure Full&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Stack Application
  &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create a full&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack app with React frontend and Node.js backend on Azure
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;team
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service

  &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application Details
      &lt;span class=&quot;token key atrule&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; name
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; description
      &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Name
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
          &lt;span class=&quot;token key atrule&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;^[a-z0-9-]+$&apos;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Description
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Infrastructure
      &lt;span class=&quot;token key atrule&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; environment
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; region
      &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Environment
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
          &lt;span class=&quot;token key atrule&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;dev&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;staging&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;production&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;region&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Azure Region
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; string
          &lt;span class=&quot;token key atrule&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;eastus&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;westus2&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;westeurope&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;enableDatabase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Include PostgreSQL Database
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; boolean
          &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;enableRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Include Redis Cache
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; boolean
          &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;base
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Fetch Base Template
      &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fetch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;template
      &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./skeleton
        &lt;span class=&quot;token key atrule&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.description &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; publish&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;github
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Publish to GitHub
      &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; publish&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;github
      &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;repoUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github.com&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;owner=crh225&lt;span class=&quot;token important&quot;&gt;&amp;amp;repo=$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.description &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;infrastructure
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Create Azure Infrastructure
      &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;backstage&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;request
      &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; POST
        &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /api/portal/deployments
        &lt;span class=&quot;token key atrule&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;blueprint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; azure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;fullstack
          &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;app_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.environment &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;region&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.region &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;enable_database&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.enableDatabase &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;enable_redis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; parameters.enableRedis &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; register&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;catalog
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Register in Catalog
      &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; catalog&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;register
      &lt;span class=&quot;token key atrule&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;repoContentsUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;publish-github&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;.output.repoContentsUrl &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;catalogInfoPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /catalog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;info.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;argocd-integration&quot;&gt;ArgoCD Integration&lt;/h3&gt;
&lt;p&gt;For Kubernetes workloads, the portal generates ArgoCD Application manifests:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Generated ArgoCD Application&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/part-of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;deployments
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;portal.chrishouse.io/blueprint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;blueprint_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;portal.chrishouse.io/deployed-by&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;portal.chrishouse.io/deployed-at&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;timestamp&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;finalizers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; resources&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;finalizer.argocd.argoproj.io
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/crh225/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.git
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; helm
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;releaseName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;valueFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; values.yaml
      &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; image.repository
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/crh225/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;app_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; image.tag
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;image_tag&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;namespace&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;syncPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;automated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;prune&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;selfHeal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;syncOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; CreateNamespace=true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;resource-discovery-finding-the-unmanaged&quot;&gt;Resource Discovery: Finding the Unmanaged&lt;/h2&gt;
&lt;p&gt;One of the most valuable features is discovering Azure resources that exist outside Terraform. The portal queries Azure Resource Graph and compares against known Terraform state:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;discoverUnmanagedResources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;subscriptionId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 1. Get all resources from Azure Resource Graph&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; azureResources &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; resourceGraph&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
    Resources
    | where subscriptionId == &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;subscriptionId&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;
    | project id, name, type, resourceGroup, location, tags
  &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 2. Get all resources from Terraform state files&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; terraformResources &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stateFiles &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listStateFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stateFile &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; stateFiles&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; state &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;downloadState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stateFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;resources&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      terraformResources&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;attributes&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 3. Find resources not in Terraform&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; unmanaged &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; azureResources&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;terraformResources&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 4. Categorize by risk level&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; unmanaged&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;riskLevel&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calculateRiskLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;estimatedCost&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;estimateResourceCost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;recommendation&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateRecommendation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calculateRiskLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// High risk: databases, key vaults, networking&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; highRiskTypes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.Sql/servers&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.KeyVault/vaults&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.Network/virtualNetworks&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.Network/publicIPAddresses&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Medium risk: compute, storage&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; mediumRiskTypes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.Compute/virtualMachines&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.Storage/storageAccounts&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Microsoft.ContainerRegistry/registries&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;highRiskTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;high&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mediumRiskTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;medium&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;low&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The UI displays unmanaged resources with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Risk level badges&lt;/strong&gt; (high/medium/low)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estimated monthly cost&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-click &quot;Import to Terraform&quot;&lt;/strong&gt; action&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership lookup&lt;/strong&gt; from tags or activity logs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cost-estimation&quot;&gt;Cost Estimation&lt;/h2&gt;
&lt;p&gt;Before any deployment, developers see estimated costs:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;estimateCost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;blueprint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pricingClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AzureRetailPricesClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Map blueprint resources to Azure pricing SKUs&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; resourceCosts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    blueprint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;resources&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sku &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resolveSkuFromParameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resource&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prices &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; pricingClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;armRegionName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;region&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;serviceFamily&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceFamily&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;skuName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; sku
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        sku&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;hourlyRate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; prices&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;retailPrice &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;monthlyEstimate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prices&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;retailPrice &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;730&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; totalMonthly &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; resourceCosts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;sum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;monthlyEstimate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resourceCosts&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    totalMonthly&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;totalYearly&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; totalMonthly &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;USD&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The PR description includes a cost breakdown:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 💰 Cost Estimate&lt;/span&gt;

&lt;span class=&quot;token table&quot;&gt;&lt;span class=&quot;token table-header-row&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; Resource &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; SKU &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; Monthly &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;----------&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-----&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;---------&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-data-rows&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; Container App &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 0.5 vCPU, 1Gi &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; $36.50 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; PostgreSQL Flexible &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; B1ms &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; $12.41 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; Storage Account &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; Standard_LRS &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; $2.30 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Total: ~$51.21/month&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token italic&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;Estimates based on Azure retail pricing. Actual costs may vary.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-resource-graph&quot;&gt;The Resource Graph&lt;/h2&gt;
&lt;p&gt;Visualizing dependencies helps developers understand what they&apos;re deploying:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Build dependency graph from Terraform state and Azure Resource Graph&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buildResourceGraph&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;deploymentName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; state &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTerraformState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;deploymentName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; edges &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; resource &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;resources&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    nodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getResourceHealth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Find dependencies from Terraform&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dependencies&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dep &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dependencies&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        edges&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; resource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instances&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attributes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; dep
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Enrich with Azure Resource Graph relationships&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; azureGraph &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; resourceGraph&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
    ResourceContainers
    | where id == &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;deploymentResourceGroup&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;
    | project-away tenantId
    | join kind=leftouter (
        Resources | project id, name, type, resourceGroup
    ) on resourceGroup
  &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; edges&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; azureGraph &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The frontend renders this as an interactive graph where developers can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click nodes to see resource details&lt;/li&gt;
&lt;li&gt;See health status (healthy/degraded/unhealthy)&lt;/li&gt;
&lt;li&gt;Trace dependencies upstream and downstream&lt;/li&gt;
&lt;li&gt;Filter by resource type or status&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;self-hosting-the-portal-deploys-itself&quot;&gt;Self-Hosting: The Portal Deploys Itself&lt;/h2&gt;
&lt;p&gt;The ultimate test: can the portal deploy itself?&lt;/p&gt;
&lt;p&gt;Yes. The Container App, Storage Account, and Front Door that host the portal were all created using portal blueprints. The bootstrap process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Initial manual setup&lt;/strong&gt;: Create resource group, storage for Terraform state, GitHub App&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy backend&lt;/strong&gt;: Use &lt;code class=&quot;language-text&quot;&gt;azure-container-app&lt;/code&gt; blueprint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy frontend&lt;/strong&gt;: Use &lt;code class=&quot;language-text&quot;&gt;azure-static-website&lt;/code&gt; blueprint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy CDN&lt;/strong&gt;: Use &lt;code class=&quot;language-text&quot;&gt;azure-front-door&lt;/code&gt; blueprint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure DNS&lt;/strong&gt;: Point &lt;code class=&quot;language-text&quot;&gt;portal.chrishouse.io&lt;/code&gt; to Front Door&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From that point, all updates go through the portal itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Blueprint design matters more than UI polish.&lt;/strong&gt; Well-designed blueprints with sensible defaults reduce form complexity dramatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GitOps is the right abstraction for infrastructure.&lt;/strong&gt; PRs provide review, rollback, and audit trails automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost visibility changes behavior.&lt;/strong&gt; When developers see &quot;$500/month&quot; before clicking deploy, they ask questions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unmanaged resource discovery is surprisingly valuable.&lt;/strong&gt; Every organization has shadow IT. Making it visible is the first step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backstage and custom portals can coexist.&lt;/strong&gt; Backstage handles catalog and scaffolding well; custom UIs handle specialized workflows better.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&apos;s Next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Policy-as-code&lt;/strong&gt;: Integrate OPA/Gatekeeper to enforce standards before PRs are created&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drift detection&lt;/strong&gt;: Alert when deployed resources diverge from Terraform state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost anomaly alerts&lt;/strong&gt;: Notify when spending exceeds estimates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-cloud&lt;/strong&gt;: Extend blueprints to AWS and GCP&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The portal is live at &lt;a href=&quot;https://portal.chrishouse.io&quot;&gt;https://portal.chrishouse.io&lt;/a&gt;—built and hosted entirely in my personal Azure lab.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[How I Built Ephemeral PR Environments on AKS]]></title><description><![CDATA[Every pull request gets its own isolated environment with HTTPS, automatic cleanup, and zero manual intervention.]]></description><link>https://chrishouse.io/ephemeral-pr-environments-aks/</link><guid isPermaLink="false">https://chrishouse.io/ephemeral-pr-environments-aks/</guid><pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;&quot;Can you deploy this to staging so I can test it?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you&apos;ve heard this a hundred times, you know the pain. Shared staging environments become bottlenecks. Developers step on each other&apos;s changes. QA can&apos;t reproduce bugs because someone else deployed over the fix.&lt;/p&gt;
&lt;p&gt;I wanted something better for my Cloud Self-Service Portal project: every pull request gets its own isolated environment, automatically, with zero manual intervention.&lt;/p&gt;
&lt;p&gt;Here&apos;s how I built it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;p&gt;When a PR is opened against my repo, GitHub Actions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Builds a Docker image tagged with the PR number&lt;/li&gt;
&lt;li&gt;Creates a dedicated Kubernetes namespace (&lt;code class=&quot;language-text&quot;&gt;armportal-pr-{PR_NUMBER}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Deploys the app with its own ingress to AKS&lt;/li&gt;
&lt;li&gt;Posts the live URL back to the PR as a comment&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When the PR is closed or merged? Everything gets cleaned up automatically.&lt;/p&gt;
&lt;p&gt;PR #42 gets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Namespace: &lt;code class=&quot;language-text&quot;&gt;armportal-pr-42&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;URL: &lt;code class=&quot;language-text&quot;&gt;https://portal-api-pr-42.pr.chrishouse.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Its own secrets, configs, and TLS certificate&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-wildcard-certificate-trick&quot;&gt;The Wildcard Certificate Trick&lt;/h2&gt;
&lt;p&gt;Here&apos;s where it gets interesting. You might think each PR environment needs its own TLS certificate. That would mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Waiting for Let&apos;s Encrypt rate limits&lt;/li&gt;
&lt;li&gt;Managing dozens of certificates&lt;/li&gt;
&lt;li&gt;Slower deployments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead, I use a single wildcard certificate for &lt;code class=&quot;language-text&quot;&gt;*.pr.chrishouse.io&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Certificate
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;wildcard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;wildcard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls
  &lt;span class=&quot;token key atrule&quot;&gt;dnsNames&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.pr.chrishouse.io&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;issuerRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dns01
    &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One certificate, unlimited PR environments. PR #1 through PR #9999 all just work.&lt;/p&gt;
&lt;p&gt;The catch? Wildcard certificates require DNS-01 validation (HTTP-01 can&apos;t verify wildcards). That&apos;s where Cloudflare comes in.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cloudflare-dns-01-challenge&quot;&gt;Cloudflare DNS-01 Challenge&lt;/h2&gt;
&lt;p&gt;Let&apos;s Encrypt needs to verify I own &lt;code class=&quot;language-text&quot;&gt;*.pr.chrishouse.io&lt;/code&gt;. With DNS-01, cert-manager automatically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creates a TXT record in Cloudflare: &lt;code class=&quot;language-text&quot;&gt;_acme-challenge.pr.chrishouse.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Let&apos;s Encrypt verifies the record exists&lt;/li&gt;
&lt;li&gt;Certificate is issued&lt;/li&gt;
&lt;li&gt;TXT record is cleaned up&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dns01
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;acme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//acme&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v02.api.letsencrypt.org/directory
    &lt;span class=&quot;token key atrule&quot;&gt;solvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;dns01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;cloudflare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;apiTokenSecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cloudflare&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
            &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Cloudflare API token lives in Azure Key Vault and gets synced to Kubernetes via a daily CronJob using the Secrets Store CSI Driver. If I rotate the token in Azure, Kubernetes picks it up automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-deployment-flow&quot;&gt;The Deployment Flow&lt;/h2&gt;
&lt;p&gt;When a PR is opened, here&apos;s what happens:&lt;/p&gt;
&lt;h3 id=&quot;1-build--push&quot;&gt;1. Build &amp;#x26; Push&lt;/h3&gt;
&lt;p&gt;Docker image gets built and pushed to Azure Container Registry with a PR-specific tag:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and push Docker image
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
    docker build -t $ACR_REGISTRY/armportal-backend:pr-${{ github.event.pull_request.number }} .
    docker push $ACR_REGISTRY/armportal-backend:pr-${{ github.event.pull_request.number }}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;2-create-namespace--copy-secrets&quot;&gt;2. Create Namespace &amp;#x26; Copy Secrets&lt;/h3&gt;
&lt;p&gt;The clever bit: I copy production secrets to the PR namespace, stripping Kubernetes metadata with &lt;code class=&quot;language-text&quot;&gt;jq&lt;/code&gt; so the apply doesn&apos;t fail. Same pattern for the wildcard TLS certificate—copy it from cert-manager namespace to the PR namespace.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get secret backend-secrets &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; armportal-backend &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; json &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  jq &lt;span class=&quot;token string&quot;&gt;&apos;del(.metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp)&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  jq &lt;span class=&quot;token string&quot;&gt;&apos;.metadata.namespace = &quot;armportal-pr-&apos;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$PR_NUMBER&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&quot;&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; -&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;3-deploy-with-templating&quot;&gt;3. Deploy with Templating&lt;/h3&gt;
&lt;p&gt;I use &lt;code class=&quot;language-text&quot;&gt;envsubst&lt;/code&gt; to inject PR-specific values into a deployment template:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;PR_NUMBER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;${{ github.event.pull_request.number }&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
envsubst &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; pr-deployment-template.yaml &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; -&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;4-post-the-url&quot;&gt;4. Post the URL&lt;/h3&gt;
&lt;p&gt;GitHub Actions comments on the PR with the live environment URL and health check endpoint:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Comment on PR
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;script@v7
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      github.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body: `## PR Environment Deployed!&lt;/span&gt;

        &lt;span class=&quot;token important&quot;&gt;**API&lt;/span&gt; URL&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;**&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;prNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.pr.chrishouse.io
        &lt;span class=&quot;token important&quot;&gt;**Health&lt;/span&gt; Check&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;**&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//portal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;prNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.pr.chrishouse.io/api/health`
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;auto-cleanup-the-unsung-hero&quot;&gt;Auto-Cleanup: The Unsung Hero&lt;/h2&gt;
&lt;p&gt;The deployment is cool. The cleanup is what makes it sustainable.&lt;/p&gt;
&lt;p&gt;When a PR is closed (merged or abandoned), GitHub Actions:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pull_request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;closed&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Delete namespace
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kubectl delete namespace armportal&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.event.pull_request.number &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Delete Docker image
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          az acr repository delete \
            --name $ACR_NAME \
            --image armportal-backend:pr-${{ github.event.pull_request.number }} \
            --yes&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;continue-on-error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deletes the Kubernetes namespace&lt;/strong&gt; — cascades to everything inside (pods, services, ingress, secrets, configmaps)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deletes the Docker image from ACR&lt;/strong&gt; — with &lt;code class=&quot;language-text&quot;&gt;continue-on-error&lt;/code&gt; so failed builds don&apos;t break cleanup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One command, complete cleanup.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cost-optimization&quot;&gt;Cost Optimization&lt;/h2&gt;
&lt;p&gt;PR environments are intentionally lightweight:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Replicas&lt;/td&gt;
&lt;td&gt;1 (no HA needed for testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU Request&lt;/td&gt;
&lt;td&gt;100m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory Request&lt;/td&gt;
&lt;td&gt;128Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;None (stateless by design)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The AKS cluster runs on a single B2s node (2 vCPU, 4GB RAM). With proper resource limits, I can run 10+ PR environments simultaneously without issues.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wildcard certs scale infinitely.&lt;/strong&gt; One certificate handles unlimited PR environments without rate limits or per-deployment delays.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DNS-01 &gt; HTTP-01 for automation.&lt;/strong&gt; No need to expose port 80 or deal with ingress routing during cert validation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Namespace-per-PR is the right abstraction.&lt;/strong&gt; Kubernetes namespaces provide perfect isolation with cascading deletes built in.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Copy secrets, don&apos;t recreate them.&lt;/strong&gt; The &lt;code class=&quot;language-text&quot;&gt;jq&lt;/code&gt; metadata stripping pattern is simple but powerful.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Post URLs to PRs.&lt;/strong&gt; Discoverability matters. If developers can&apos;t find the environment, they won&apos;t use it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-result&quot;&gt;The Result&lt;/h2&gt;
&lt;p&gt;Every PR now gets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A live, isolated environment in ~2 minutes&lt;/li&gt;
&lt;li&gt;HTTPS with valid certificates&lt;/li&gt;
&lt;li&gt;Automatic cleanup when merged/closed&lt;/li&gt;
&lt;li&gt;Zero manual intervention required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reviewers can test changes in production-like conditions. QA can reproduce bugs on the exact commit. No more &quot;works on my machine.&quot;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Cyber Mario — Public Testing Beta]]></title><description><![CDATA[Neon platforming, SMW-style scoring, sub-levels, and controller support — all running in your browser.]]></description><link>https://chrishouse.io/cyber-mario-testing-beta/</link><guid isPermaLink="false">https://chrishouse.io/cyber-mario-testing-beta/</guid><pubDate>Sun, 14 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to the &lt;strong&gt;Cyber Mario&lt;/strong&gt; testing beta — a neon-soaked platformer engineered for the open web.&lt;br&gt;
It’s fast, punchy, and built to feel great on both keyboard and gamepad, desktop or phone.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-play-the-beta&quot;&gt;🚀 Play the Beta&lt;/h2&gt;
&lt;p&gt;The game runs right here, in the blog — no install, no sign-in.&lt;br&gt;
&lt;strong&gt;Launch it here → &lt;a href=&quot;/cyber-mario&quot;&gt;Play Cyber Mario&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Grab a controller if you’ve got one. Press &lt;strong&gt;~&lt;/strong&gt; to open the map.&lt;br&gt;
See how far you can push your chain, and tag your best flag-pole bonus.&lt;/p&gt;
&lt;p&gt;Neon on. Coins up. See you in &lt;strong&gt;World 1-1&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-how-to-play&quot;&gt;🎮 How to Play&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Move:&lt;/strong&gt; Arrow keys / &lt;strong&gt;A-D&lt;/strong&gt; or left stick&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jump:&lt;/strong&gt; &lt;strong&gt;Z&lt;/strong&gt; / &lt;strong&gt;Space&lt;/strong&gt; / &lt;strong&gt;A&lt;/strong&gt; (Xbox)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fire (when powered):&lt;/strong&gt; &lt;strong&gt;X&lt;/strong&gt; (Xbox) / &lt;strong&gt;K&lt;/strong&gt; (keyboard)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slam:&lt;/strong&gt; Press &lt;strong&gt;Down&lt;/strong&gt; while in the air&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;World Map:&lt;/strong&gt; Press &lt;strong&gt;~&lt;/strong&gt; (Backquote) or click &lt;strong&gt;World Map ▾&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reserve power-up (SMW-style):&lt;/strong&gt; Press &lt;strong&gt;R&lt;/strong&gt; to drop from the top-center box&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;strong&gt;HUD&lt;/strong&gt; keeps things clean: hearts, time, coins, and your running score. A lightweight profiler HUD (Ctrl+Shift+D) is available for devs who want frame timing and draw-call stats.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-whats-new-in-the-beta&quot;&gt;✨ What’s New in the Beta&lt;/h2&gt;
&lt;h3 id=&quot;a-smw-inspired-reserve-box&quot;&gt;A SMW-inspired Reserve Box&lt;/h3&gt;
&lt;p&gt;Pick up a mushroom/wing/fire while you’re already powered? It gets tucked into a &lt;strong&gt;top-center reserve box&lt;/strong&gt;. Tap/click it or press &lt;strong&gt;R&lt;/strong&gt; to deploy — perfect for clutch saves or boss runs.&lt;/p&gt;
&lt;h3 id=&quot;world-map-with-true-unlocks&quot;&gt;World Map with True Unlocks&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;World Map&lt;/strong&gt; auto-filters and shows &lt;strong&gt;lock icons&lt;/strong&gt; on levels you haven’t earned yet. Beat 1-2 to unlock 1-3, and so on — first level in each world is always open. Locks match the castle lock styling for a consistent vibe.&lt;/p&gt;
&lt;h3 id=&quot;course-clear-summary-single-modal&quot;&gt;Course-Clear Summary (Single Modal)&lt;/h3&gt;
&lt;p&gt;Finish a level and see a &lt;strong&gt;single, consolidated summary&lt;/strong&gt;: coins, enemy KOs, chain max, flag bonus, time bonus, clear bonus, and the total. From there, confirm and roll into the next course.&lt;/p&gt;
&lt;h3 id=&quot;controller-ux-on-desktop&quot;&gt;Controller UX on Desktop&lt;/h3&gt;
&lt;p&gt;A small &lt;strong&gt;controller indicator&lt;/strong&gt; sits at the top-right: grey when disconnected, &lt;strong&gt;cyber-lime&lt;/strong&gt; when a pad is live. You’ll also get &lt;strong&gt;toasts&lt;/strong&gt; on connect/disconnect and a &lt;strong&gt;one-click controls sheet&lt;/strong&gt; tailored for Xbox mapping.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-design-notes&quot;&gt;🧠 Design Notes&lt;/h2&gt;
&lt;h3 id=&quot;controls-that-forgive-and-reward&quot;&gt;Controls That Forgive (and Reward)&lt;/h3&gt;
&lt;p&gt;The jump logic includes &lt;strong&gt;coyote time&lt;/strong&gt; and a &lt;strong&gt;jump buffer&lt;/strong&gt;, so late jumps still register, and you can chain stomps or slam attacks without fighting your inputs.&lt;/p&gt;
&lt;h3 id=&quot;scoring-that-feels-right&quot;&gt;Scoring That Feels Right&lt;/h3&gt;
&lt;p&gt;Like SMW, score is &lt;strong&gt;lifetime-style&lt;/strong&gt; across a run and tallies &lt;strong&gt;only&lt;/strong&gt; when you clear a course. Sub-levels contribute to their parent; dying mid-course won’t corrupt your saved totals.&lt;/p&gt;
&lt;h3 id=&quot;sub-levels--checkpoints&quot;&gt;Sub-Levels &amp;#x26; Checkpoints&lt;/h3&gt;
&lt;p&gt;Ducts, pipes, and side rooms act as &lt;strong&gt;segments&lt;/strong&gt;; their loot and KOs feed the parent level’s totals. Checkpoint flags provide a one-time respawn with remaining time preserved.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-under-the-hood-for-the-curious&quot;&gt;📟 Tech Under the Hood (for the curious)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure Canvas + React&lt;/strong&gt; single page with careful draw batching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PWA niceties:&lt;/strong&gt; manifest, theme color, iOS icons, and full-screen helpers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile polish:&lt;/strong&gt; orientation nudge, input-zoom prevention, touch-gesture hard locks, and dynamic viewport sizing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local storage:&lt;/strong&gt; world-clear state and scoring durability live client-side now, with a clean path to a server later&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance tools:&lt;/strong&gt; an optional on-screen &lt;strong&gt;frame budget profiler&lt;/strong&gt; for FPS/ms and tile/draw counts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(If you like peeking behind the curtain, many of these features are wired directly in &lt;code class=&quot;language-text&quot;&gt;cyber-mario.js&lt;/code&gt; — reserve box, world map, controller UI, summary modal, mobile UX helpers, and more.)&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-progression--unlocks&quot;&gt;🗺 Progression &amp;#x26; Unlocks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First level of each world&lt;/strong&gt; is always playable.&lt;/li&gt;
&lt;li&gt;Subsequent levels unlock once you’ve &lt;strong&gt;cleared the previous&lt;/strong&gt; main level.&lt;/li&gt;
&lt;li&gt;Sub-levels &lt;strong&gt;never&lt;/strong&gt; appear in the map and always &lt;strong&gt;roll up&lt;/strong&gt; to their parent.&lt;/li&gt;
&lt;li&gt;Your cleared worlds persist in &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;cm_worlds_cleared_v1&lt;/code&gt;&lt;/strong&gt; (browser storage).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-audio--feel&quot;&gt;🔊 Audio &amp;#x26; Feel&lt;/h2&gt;
&lt;p&gt;A chill chiptune loop sets the mood. Jumps, wins, and the occasional Boo cackle round out the soundscape. Toggle BGM on/off from the HUD anytime.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-devices--performance&quot;&gt;📱 Devices &amp;#x26; Performance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desktop:&lt;/strong&gt; Chrome or Edge recommended; gamepad strongly supported.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS/Android:&lt;/strong&gt; Mobile-first tweaks reduce accidental pinch-zoom and keep the canvas full-bleed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debug HUD:&lt;/strong&gt; Enable when needed; it’s hidden by default so players stay immersed.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-to-test-right-now&quot;&gt;🧪 What to Test Right Now&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Map unlocks after clearing a level (1-2 → 1-3, etc.)&lt;/li&gt;
&lt;li&gt;Reserve box behavior when picking up a second power-up&lt;/li&gt;
&lt;li&gt;Course-clear modal totals vs HUD totals&lt;/li&gt;
&lt;li&gt;Gamepad mapping: &lt;strong&gt;A = Jump&lt;/strong&gt;, &lt;strong&gt;X = Fire&lt;/strong&gt;; slam + world-map shortcuts&lt;/li&gt;
&lt;li&gt;Sub-level coin/score roll-up into the parent course&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you spot glitches, capture a short clip or screenshot plus the &lt;strong&gt;level ID&lt;/strong&gt; and &lt;strong&gt;what you were doing&lt;/strong&gt; just before it happened.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-roadmap&quot;&gt;🛣 Roadmap&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Boss pass with telegraphed patterns and arena locks&lt;/li&gt;
&lt;li&gt;Particle polish on stomps, bricks, and flag bonuses&lt;/li&gt;
&lt;li&gt;Optional leaderboards (opt-in, seed-based submission)&lt;/li&gt;
&lt;li&gt;Achievements and time-attack variants&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</content:encoded></item><item><title><![CDATA[Terminal Hacker — Season 1: Access Granted]]></title><description><![CDATA[Introducing Terminal Hacker, a retro-inspired browser game hidden in this blog. Learn how it works, why it's fun, and what challenges await across Seasons 1–5.]]></description><link>https://chrishouse.io/terminal-hacker-boot-the-box/</link><guid isPermaLink="false">https://chrishouse.io/terminal-hacker-boot-the-box/</guid><pubDate>Sat, 30 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to &lt;strong&gt;Terminal Hacker&lt;/strong&gt;, a retro-inspired browser game hidden
right inside this blog.&lt;/p&gt;
&lt;p&gt;It looks like a terminal.&lt;br&gt;
It feels like a terminal.&lt;br&gt;
But behind the prompt lies a season-based hacking challenge with secrets,
puzzles, and cheeky Easter eggs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-how-it-works&quot;&gt;🎮 How it Works&lt;/h2&gt;
&lt;p&gt;Terminal Hacker simulates a &lt;strong&gt;Unix-like shell environment&lt;/strong&gt; directly in your browser.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type commands&lt;/strong&gt; at the prompt, just like you would on Linux.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explore a file system&lt;/strong&gt; with &lt;code class=&quot;language-text&quot;&gt;/etc&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/var/log&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/home/guest&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;systemd&lt;/code&gt; unit files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Investigate logs&lt;/strong&gt; in &lt;code class=&quot;language-text&quot;&gt;/var/log/journal&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;/var/log/pods&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summon raptors&lt;/strong&gt; (yes, really).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track your progress&lt;/strong&gt; with built-in stats and sharing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your progress is stored in browser local storage — no backend, no signup.&lt;br&gt;
When you run &lt;code class=&quot;language-text&quot;&gt;shutdown&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;reboot&lt;/code&gt;, your progress resets (but your &lt;strong&gt;seed identity&lt;/strong&gt; persists).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-core-experience&quot;&gt;📟 The Core Experience&lt;/h2&gt;
&lt;p&gt;Every command counts toward progress:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commands run&lt;/li&gt;
&lt;li&gt;Discoveries made&lt;/li&gt;
&lt;li&gt;Uptime kept&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At any time you can generate a shareable ASCII card of your stats.&lt;br&gt;
Copy, paste, brag.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-season-1-access-granted&quot;&gt;✨ Season 1: Access Granted&lt;/h2&gt;
&lt;p&gt;This is the inaugural season.&lt;br&gt;
Your mission: &lt;strong&gt;Access Granted, explore, and uncover as much as possible before the season ends.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are commands to master, logs to read, and secrets hidden in unexpected places.&lt;br&gt;
No walkthroughs. No spoilers. Just your wits and curiosity.&lt;/p&gt;
&lt;p&gt;At the end of your run, you’ll have a seed like &lt;code class=&quot;language-text&quot;&gt;CH-AB12CD&lt;/code&gt; —&lt;br&gt;
your hacker ID for Season 1.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-badges-season-1&quot;&gt;🏅 Badges (Season 1)&lt;/h2&gt;
&lt;p&gt;Season 1 includes five badges to discover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explorer&lt;/li&gt;
&lt;li&gt;Archivist&lt;/li&gt;
&lt;li&gt;Summoner&lt;/li&gt;
&lt;li&gt;Bootloader Clear&lt;/li&gt;
&lt;li&gt;Chronos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Collect them all to maximize your progress.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-command-reference-highlights&quot;&gt;📚 Command Reference (Highlights)&lt;/h2&gt;
&lt;h3 id=&quot;navigation--file-ops&quot;&gt;Navigation &amp;#x26; File Ops&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ls&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cd&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pwd&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cat&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;touch&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;echo&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;tee&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mkdir&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;rm&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mv&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cp&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;search--logs&quot;&gt;Search &amp;#x26; Logs&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;grep -R&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;find&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;locate&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;journalctl&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;systemctl&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;klogs&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;fun--flavor&quot;&gt;Fun &amp;#x26; Flavor&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;fortune&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;raptor&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;bootloader&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;qte&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;hint&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;share&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sudo&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;uptime&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;uuid&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cal&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;random&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-resetting-the-box&quot;&gt;🔄 Resetting the Box&lt;/h2&gt;
&lt;p&gt;Sometimes you dig too deep, delete the wrong thing, or just want to try a different path.&lt;br&gt;
That’s what the reset commands are for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;reboot&lt;/code&gt; — restart the system, keep your seed.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;restart&lt;/code&gt; — alias for reboot.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;shutdown&lt;/code&gt; — power off and clear progress.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your &lt;strong&gt;seed identity&lt;/strong&gt; always remains. Everything else — files, notes, progress — resets.&lt;br&gt;
Think of it as wiping your VM clean and starting fresh.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-hints-and-sharing&quot;&gt;💡 Hints and Sharing&lt;/h2&gt;
&lt;p&gt;Hints aren’t free — you have to earn them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Type &lt;code class=&quot;language-text&quot;&gt;hint&lt;/code&gt; and you’ll get a random clue.&lt;/li&gt;
&lt;li&gt;When you run out, type &lt;code class=&quot;language-text&quot;&gt;share&lt;/code&gt; to generate an ASCII stats card.&lt;/li&gt;
&lt;li&gt;Sharing it earns you &lt;strong&gt;one more hint&lt;/strong&gt; for your run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best players use hints sparingly — because the real fun is discovery.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-why-its-fun&quot;&gt;🕹 Why It’s Fun&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feels real.&lt;/strong&gt; Boot messages, system logs, and file structures make it feel alive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Playful surprises.&lt;/strong&gt; Raptors, boss fights, hidden commands.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hacky nostalgia.&lt;/strong&gt; If you’ve ever SSH’d into a Raspberry Pi, it feels familiar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Social bragging.&lt;/strong&gt; Share your ASCII stats card — your hacker cred in monospace glory.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-seasons-roadmap&quot;&gt;🗓 Seasons Roadmap&lt;/h2&gt;
&lt;p&gt;Terminal Hacker isn’t just one box — it’s a saga.&lt;/p&gt;
&lt;h3 id=&quot;-season-2--daemon-dance&quot;&gt;🔥 Season 2 — Daemon Dance&lt;/h3&gt;
&lt;p&gt;Services come alive. Pods get noisier, and logs whisper secrets.&lt;/p&gt;
&lt;h3 id=&quot;-season-3--network-intrigue&quot;&gt;🌐 Season 3 — Network Intrigue&lt;/h3&gt;
&lt;p&gt;Networking commands appear. Hidden hosts respond.&lt;/p&gt;
&lt;h3 id=&quot;-season-4--container-chaos&quot;&gt;📦 Season 4 — Container Chaos&lt;/h3&gt;
&lt;p&gt;Containers and pods expand. Debugging becomes the game.&lt;/p&gt;
&lt;h3 id=&quot;-season-5--final-root&quot;&gt;👑 Season 5 — Final Root&lt;/h3&gt;
&lt;p&gt;Root secrets, privilege escalation, and a final boss.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-coming-soon-features&quot;&gt;🚀 Coming Soon Features&lt;/h2&gt;
&lt;p&gt;Beyond the seasons, bigger features are in the works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leaderboards&lt;/strong&gt; — global ranking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Score submission&lt;/strong&gt; — secure seed + stats upload&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tamper-proofing&lt;/strong&gt; — scores can’t be faked&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-whats-next&quot;&gt;🎯 What’s Next?&lt;/h2&gt;
&lt;p&gt;Your seed will carry forward.&lt;br&gt;
Progress stacks season over season, building your hacker legacy.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-final-teaser&quot;&gt;🔮 Final Teaser&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The raptor is just the beginning.&lt;br&gt;
The real fun starts when the box fights back.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Access Granted.&lt;br&gt;
Crack the system.&lt;br&gt;
And get ready for &lt;strong&gt;Daemon Dance&lt;/strong&gt; in Season 2.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[From Raspberry Pi to the Cloud: Publishing Kubernetes Dashboard Securely with Cloudflare Tunnel]]></title><description><![CDATA[How I used a Raspberry Pi, K3s, and Cloudflare Tunnel to securely publish the Kubernetes Dashboard without port forwarding.]]></description><link>https://chrishouse.io/pi-to-cloud-kubernetes-dashboard/</link><guid isPermaLink="false">https://chrishouse.io/pi-to-cloud-kubernetes-dashboard/</guid><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the weekend I decided to take my Raspberry Pi 5 a step further and turn it into a Kubernetes playground. My goal was to get the Kubernetes Dashboard up and running and then make it securely accessible from the internet, without relying on port forwarding or static IPs. The tools I chose were &lt;strong&gt;K3s&lt;/strong&gt; for the cluster, &lt;strong&gt;Cloudflare DNS&lt;/strong&gt; for my domain (&lt;code class=&quot;language-text&quot;&gt;chrishouse.io&lt;/code&gt;), and &lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; to publish the service safely.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;installing-k3s&quot;&gt;Installing K3s&lt;/h2&gt;
&lt;p&gt;The first step was the cluster itself. I initially tried MicroK8s but hit snapd version issues, so I pivoted to K3s which installed quickly. On the Pi I ran:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sfL&lt;/span&gt; https://get.k3s.io &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt; -&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A quick check confirmed my node was live:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; k3s kubectl get &lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;deploying-the-kubernetes-dashboard&quot;&gt;Deploying the Kubernetes Dashboard&lt;/h2&gt;
&lt;p&gt;Once the cluster was healthy, I deployed the Kubernetes Dashboard. The manifests are published by the project, so installation was straightforward. The service exposed itself via a &lt;strong&gt;NodePort&lt;/strong&gt; on &lt;code class=&quot;language-text&quot;&gt;30001&lt;/code&gt;, and I verified it was working locally:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; k3s kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kubernetes-dashboard get svc&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Which returned:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;kubernetes-dashboard   NodePort   10.43.6.40   &amp;lt;none&gt;   443:30001/TCP&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From there I tested directly on the Pi:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vk&lt;/span&gt; https://localhost:30001&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The output confirmed a TLS handshake with a self-signed certificate and an HTTP 200 response containing the HTML of the dashboard login page.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;adding-cloudflare-dns-and-tunnel&quot;&gt;Adding Cloudflare DNS and Tunnel&lt;/h2&gt;
&lt;p&gt;The challenge came when I wanted to expose this beyond my LAN. My ISP provides a dynamic IP, my Orbi router doesn’t like giving up ports 80 and 443, and I didn’t want to maintain fragile port forwarding rules anyway. That’s when I turned to &lt;strong&gt;Cloudflare&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I had already registered a domain (&lt;code class=&quot;language-text&quot;&gt;chrishouse.io&lt;/code&gt;) and moved DNS into Cloudflare. From there I installed &lt;code class=&quot;language-text&quot;&gt;cloudflared&lt;/code&gt; on the Pi:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://pkg.cloudflare.com/cloudflare-main.gpg &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /usr/share/keyrings/cloudflare-main.gpg &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared jammy main&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/cloudflared.list
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; cloudflared &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After installation, I authenticated it against my Cloudflare account:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;cloudflared tunnel login&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That opened a browser window where I logged in and selected my domain. Credentials were saved to &lt;code class=&quot;language-text&quot;&gt;~/.cloudflared/cert.pem&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I created a named tunnel for the dashboard:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;cloudflared tunnel create k3s-dashboard&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And mapped it to my subdomain:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;cloudflared tunnel route dns k3s-dashboard dashboard.chrishouse.io&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This created a &lt;strong&gt;CNAME record&lt;/strong&gt; in Cloudflare pointing &lt;code class=&quot;language-text&quot;&gt;dashboard.chrishouse.io&lt;/code&gt; at the tunnel rather than my home IP.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;configuring-the-tunnel&quot;&gt;Configuring the Tunnel&lt;/h2&gt;
&lt;p&gt;The last piece was configuring the tunnel itself. I created a config file:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; /etc/cloudflared/config.yml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With the contents:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;tunnel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; k3s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dashboard
&lt;span class=&quot;token key atrule&quot;&gt;credentials-file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /home/chris/.cloudflared/&amp;lt;tunnel&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;.json

&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dashboard.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;30001&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;originRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;noTLSVerify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http_status&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The critical setting here was &lt;code class=&quot;language-text&quot;&gt;noTLSVerify: true&lt;/code&gt; because the Dashboard serves HTTPS with a self-signed certificate. Without that, Cloudflared would reject the connection.&lt;/p&gt;
&lt;p&gt;Finally, I installed it as a service:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; cloudflared &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; cloudflared
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start cloudflared&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And checked the logs with:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; journalctl &lt;span class=&quot;token parameter variable&quot;&gt;-u&lt;/span&gt; cloudflared &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The logs showed multiple connections registered to Cloudflare edge locations. At that point, opening &lt;a href=&quot;https://dashboard.chrishouse.io&quot;&gt;https://dashboard.chrishouse.io&lt;/a&gt; from my browser worked flawlessly.&lt;/p&gt;
&lt;p&gt;Instead of a self-signed cert, the connection terminated at Cloudflare’s edge with a &lt;strong&gt;valid certificate&lt;/strong&gt;, and the dashboard login page loaded securely.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;What started as an experiment in running Kubernetes on a Raspberry Pi ended up being a deep dive into modern networking. By leveraging Cloudflare Tunnel, I didn’t have to expose my home IP, configure NAT rules on my router, or worry about whether my ISP changed my IP overnight. My Raspberry Pi simply maintained an outbound connection to Cloudflare’s global edge, and Cloudflare handled TLS termination and routing.&lt;/p&gt;
&lt;p&gt;It’s remarkable that with a $60 Raspberry Pi, an open-source Kubernetes distribution, and a free Cloudflare account, I was able to publish a production-grade web application securely to the internet.&lt;/p&gt;
&lt;p&gt;This approach doesn’t just apply to Kubernetes — the same pattern works for &lt;strong&gt;any internal service&lt;/strong&gt; you want to access remotely without punching holes in your firewall.&lt;/p&gt;
&lt;p&gt;As a related project, I’ve also been experimenting with &lt;strong&gt;Meshtastic&lt;/strong&gt; and building a live mesh radio map for the Memphis area. That project, called &lt;em&gt;Memphis Meshview&lt;/em&gt;, takes real-time MQTT packet data from local Meshtastic nodes and renders them on an interactive map. If you’d like to see a working example of how these lightweight IoT networks can be visualized in real time, you can check it out at &lt;a href=&quot;https://meshview.chrishouse.io/map&quot;&gt;https://meshview.chrishouse.io/map&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Beyond maps, it also provides a persistent conversation log where messages exchanged across the mesh network are stored and browsable at &lt;a href=&quot;https://meshview.chrishouse.io/chat&quot;&gt;https://meshview.chrishouse.io/chat&lt;/a&gt;. It’s a live example of how these techniques can be used to make distributed, community-driven networks visible and interactive on the web.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Memphis Meshview Project: Visualizing Meshtastic with Maps and Chat Logs]]></title><description><![CDATA[How the Memphis Meshview project integrates Meshtastic MQTT messages into a live map and persistent chat logs, giving the community real-time insight into the mesh.]]></description><link>https://chrishouse.io/memphis-mesh/</link><guid isPermaLink="false">https://chrishouse.io/memphis-mesh/</guid><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Meshtastic makes off-grid communication possible by linking LoRa radios into a self-healing mesh. But once you connect nodes to &lt;strong&gt;MQTT&lt;/strong&gt;, the game changes — your local mesh can bridge into a global one, with all messages and positions flowing through a central broker.&lt;/p&gt;
&lt;p&gt;This inspired the &lt;strong&gt;Memphis Meshview project&lt;/strong&gt;, where I set up a dashboard to visualize Meshtastic activity around Memphis. The goal was twofold:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Map&lt;/strong&gt; the nodes reporting in via MQTT.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persist chat logs&lt;/strong&gt; for historical reference.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Together, this creates a living snapshot of the mesh — a community map where you can see who’s online, where they are, and what messages are flowing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mqtt-as-the-backbone&quot;&gt;MQTT as the Backbone&lt;/h2&gt;
&lt;p&gt;Meshtastic nodes normally just talk directly over LoRa. But when a node has internet access, it can &lt;strong&gt;publish messages to an MQTT broker&lt;/strong&gt; (and subscribe back).&lt;/p&gt;
&lt;p&gt;In our case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;broker&lt;/strong&gt; connects to the Memphis namespace (&lt;code class=&quot;language-text&quot;&gt;msh/US/memphisme.sh&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Every node that has MQTT enabled forwards its messages there.&lt;/li&gt;
&lt;li&gt;Other MQTT-connected nodes receive the same traffic, even if they’re physically too far apart to hear each other over radio.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a &lt;strong&gt;virtual backbone&lt;/strong&gt; that ties together distant or isolated clusters.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;building-the-live-map&quot;&gt;Building the Live Map&lt;/h2&gt;
&lt;p&gt;The map piece uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node coordinates&lt;/strong&gt;: Many Meshtastic devices can report GPS location.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frontend visualization&lt;/strong&gt;: A Leaflet.js-based map hosted at &lt;a href=&quot;https://meshview.chrishouse.io/map&quot;&gt;meshview.chrishouse.io/map&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend logic&lt;/strong&gt;: MQTT subscriptions feed updates into a datastore, which then updates the map markers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each node shows up as a point, with metadata like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Callsign or device name&lt;/li&gt;
&lt;li&gt;Last seen time&lt;/li&gt;
&lt;li&gt;Altitude (when reported)&lt;/li&gt;
&lt;li&gt;Whether it came in &lt;strong&gt;via radio&lt;/strong&gt; or &lt;strong&gt;via MQTT&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This lets the community quickly see coverage holes and hotspots.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;persistent-chat-logs&quot;&gt;Persistent Chat Logs&lt;/h2&gt;
&lt;p&gt;Beyond mapping, I wanted &lt;strong&gt;conversation history&lt;/strong&gt;. Normally, Meshtastic messages are ephemeral — if you miss them live, they’re gone.&lt;/p&gt;
&lt;p&gt;To fix that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I subscribed to the MQTT chat topics.&lt;/li&gt;
&lt;li&gt;Messages were written into a database with timestamps, user IDs, and message content.&lt;/li&gt;
&lt;li&gt;A simple web front-end at &lt;a href=&quot;https://meshview.chrishouse.io/chat&quot;&gt;meshview.chrishouse.io/chat&lt;/a&gt; lets you scroll back through the conversation log.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This made the mesh &lt;strong&gt;feel more like a living community&lt;/strong&gt;, not just random bursts of radio packets.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MQTT is a Force Multiplier&lt;/strong&gt;&lt;br&gt;
Even if your local radio range is small, MQTT bridges make the mesh global. One solar node in Memphis can trade messages with a handheld in Colorado.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Normalization Matters&lt;/strong&gt;&lt;br&gt;
Different nodes send different fields. Some send GPS reliably, others don’t. Building a clean map means handling messy input gracefully.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Persistence Changes Behavior&lt;/strong&gt;&lt;br&gt;
Once people knew there was a persistent chat log, conversations became more thoughtful — it shifted from ephemeral pings to community discussions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Visualization Inspires Growth&lt;/strong&gt;&lt;br&gt;
The map encouraged others to light up their nodes, just to “see themselves” show up in Memphis Meshview. A visible network drives participation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Meshtastic’s beauty lies in its simplicity: radios talking to each other with no infrastructure. But once you add MQTT, you unlock a shared digital layer that lets local meshes become part of a &lt;strong&gt;bigger story&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The Memphis Meshview project is one example — a mix of mapping, chat logging, and visualization that makes the invisible mesh visible. And in a world where off-grid communication matters more every year, seeing the network grow in real time is as inspiring as it is useful.&lt;/p&gt;
&lt;hr&gt;</content:encoded></item><item><title><![CDATA[Building a Personal Mesh Network with Meshtastic]]></title><description><![CDATA[How I built a 3-node personal mesh network using Meshtastic, including a mobile, solar-powered, and desk node — plus lessons learned about node roles, placement, and settings.]]></description><link>https://chrishouse.io/personal-mesh-network/</link><guid isPermaLink="false">https://chrishouse.io/personal-mesh-network/</guid><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Mesh networking has always fascinated me. The idea that devices can form their own network without relying on cell towers, Wi-Fi, or even the internet is powerful — especially in rural or off-grid scenarios.&lt;/p&gt;
&lt;p&gt;Over the last few weeks, I set out to build a &lt;strong&gt;personal 3-node Meshtastic network&lt;/strong&gt; to learn more about LoRa radios, MQTT integrations, and practical deployment considerations. My setup included:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;mobile node&lt;/strong&gt; (car / handheld).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;solar-powered outdoor node&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;desk node&lt;/strong&gt; that always listens for messages.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s what I learned about node roles, placement, and how to avoid common mistakes.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;node-setup&quot;&gt;Node Setup&lt;/h2&gt;
&lt;h3 id=&quot;1-mobile-node-car--portable&quot;&gt;1. Mobile Node (Car / Portable)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hardware: Heltec LoRa V3 (with onboard battery charging via USB-C).&lt;/li&gt;
&lt;li&gt;Role: &lt;strong&gt;Client&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Why: Mobile nodes move frequently and don’t provide stable routing. They’re best kept as clients that connect to stronger repeater or router nodes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-solar-powered-outdoor-node&quot;&gt;2. Solar-Powered Outdoor Node&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hardware: Heltec LoRa board + small LiPo battery + 10W solar panel with charge controller.&lt;/li&gt;
&lt;li&gt;Placement: Mounted ~30 ft on my roofline.&lt;/li&gt;
&lt;li&gt;Role: Initially tested as &lt;strong&gt;Router&lt;/strong&gt;, but later kept as &lt;strong&gt;Client&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Lesson: Unless you can mount an antenna very high (100 ft above ground level or better), routers often just add network noise. A client role avoids unnecessary re-broadcast loops while still being available when powered.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-desk-node-always-on-receiver&quot;&gt;3. Desk Node (Always-On Receiver)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hardware: Heltec LoRa board powered by USB-C on my desk.&lt;/li&gt;
&lt;li&gt;Role: &lt;strong&gt;Client&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Why: It doesn’t need to forward messages — it simply provides a stable “listener” for monitoring the network.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;understanding-node-roles&quot;&gt;Understanding Node Roles&lt;/h2&gt;
&lt;p&gt;Meshtastic supports different roles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt; → Best for most setups. Receives and transmits messages but doesn’t rebroadcast everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Router&lt;/strong&gt; → For very high, clear-line-of-sight placements (e.g., mountaintops, tall towers). Always rebroadcasts, ensuring wide coverage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeater&lt;/strong&gt; → Similar to router, but specifically designed to extend coverage in areas where clients cannot directly connect. Needs &lt;strong&gt;very good placement&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice:&lt;br&gt;
👉 Unless you can get your antenna &lt;strong&gt;very high with clear tree lines&lt;/strong&gt;, stick to &lt;strong&gt;Client&lt;/strong&gt; mode. Misconfigured routers at low height can actually hurt the mesh by creating redundant traffic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Antenna Height Matters More Than Power&lt;/strong&gt;&lt;br&gt;
My 30 ft rooftop node wasn’t high enough to justify being a router. At lower heights, the Fresnel zone gets blocked by trees, houses, and terrain.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Battery Management Is Built In&lt;/strong&gt;&lt;br&gt;
Boards like the Heltec LoRa V3 include a TP4056-style charging circuit. Plug in USB (or solar with charge controller), and the board manages the LiPo charging automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don’t Overcomplicate Roles&lt;/strong&gt;&lt;br&gt;
Many people think they need a router/repeater to make their mesh “better.” In reality, most personal networks run best with &lt;strong&gt;all nodes as clients&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Long Range Settings (LoRa Profiles)&lt;/strong&gt;&lt;br&gt;
Meshtastic includes “LongFast” and “LongSlow” profiles.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LongFast&lt;/strong&gt; → Good balance of speed and range.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LongSlow&lt;/strong&gt; → Extreme range, but very slow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“VeryLong” was removed; “LongSlow” is now the equivalent option.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MQTT Adds Reach Beyond Radio Range&lt;/strong&gt;&lt;br&gt;
While not needed for my personal 3-node test, Meshtastic can bridge over MQTT to let remote nodes join the mesh. Useful if your physical range is limited.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Test larger solar setups to keep outdoor nodes alive through winter.&lt;/li&gt;
&lt;li&gt;Add GPS tracking to the mobile node.&lt;/li&gt;
&lt;li&gt;Experiment with MQTT to bridge my desk node into the wider Memphis Meshview community project.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Building a personal mesh network taught me that &lt;strong&gt;simplicity wins&lt;/strong&gt;: keep most nodes as clients, get antennas as high as you can, and don’t overcomplicate the setup.&lt;/p&gt;
&lt;p&gt;Even a small 3-node network is surprisingly capable — messages reliably hop between my car, roof, and desk. Combined with solar power, this little experiment has shown me the potential of LoRa-based mesh systems for both hobbyist fun and serious off-grid communication.&lt;/p&gt;
&lt;hr&gt;</content:encoded></item><item><title><![CDATA[How I Built and Deployed My Gatsby Blog on Raspberry Pi with Docker, Helm, and Kubernetes]]></title><description><![CDATA[A deep dive into how I built this Gatsby-powered blog, Dockerized it, pushed it to my Raspberry Pi container registry, and deployed it securely via Helm on K3s.]]></description><link>https://chrishouse.io/pi-gatsby-blog-setup/</link><guid isPermaLink="false">https://chrishouse.io/pi-gatsby-blog-setup/</guid><pubDate>Wed, 20 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This blog is both a playground and a living documentation platform for my experiments.&lt;br&gt;
It’s running directly on a &lt;strong&gt;Raspberry Pi 5&lt;/strong&gt; sitting on my desk. I use &lt;strong&gt;Gatsby.js&lt;/strong&gt; as the static site generator, Docker for packaging, &lt;strong&gt;K3s (a lightweight Kubernetes distribution)&lt;/strong&gt; for orchestration, and &lt;strong&gt;Helm&lt;/strong&gt; to make deployments repeatable. Everything is exposed securely via &lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This post is a deep dive into how I put this together — not just the commands, but the reasoning behind each step, the pitfalls I hit, and the lessons learned along the way.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-why-raspberry-pi-and-kubernetes&quot;&gt;1. Why Raspberry Pi and Kubernetes?&lt;/h2&gt;
&lt;p&gt;Before diving into commands, let’s talk about &lt;em&gt;why&lt;/em&gt; I even did this.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi 5&lt;/strong&gt;: It’s cheap ($60), ARM64-based, and powerful enough to run real workloads. With 8GB RAM, it can host Kubernetes pods and serve real traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K3s&lt;/strong&gt;: Normal Kubernetes is overkill for a Pi. K3s is trimmed down, lightweight, and optimized for edge devices and IoT. Perfect for homelabs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gatsby.js&lt;/strong&gt;: Static site generators make blogs blazing fast, secure, and easy to deploy as containers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker + Helm&lt;/strong&gt;: Docker lets me package my site so it runs the same everywhere. Helm makes redeployment a one-liner instead of managing multiple &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; configs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So this stack is about &lt;strong&gt;learning&lt;/strong&gt; and also proving that a $60 Pi can host a “production-grade” site.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-setting-up-k3s-on-the-pi&quot;&gt;2. Setting up K3s on the Pi&lt;/h2&gt;
&lt;p&gt;I originally tried MicroK8s, but ran into snapd version issues on Raspberry Pi OS. K3s was dead simple:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sfL&lt;/span&gt; https://get.k3s.io &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt; -&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check the node:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; k3s kubectl get nodes &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; wide&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This gives you the single-node cluster ready to schedule workloads.&lt;/p&gt;
&lt;p&gt;Key notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K3s bundles containerd by default (no need for Docker runtime).&lt;/li&gt;
&lt;li&gt;NodePorts and LoadBalancers just work.&lt;/li&gt;
&lt;li&gt;Systemd integration makes K3s restart automatically on reboot.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-building-the-blog-with-gatsby&quot;&gt;3. Building the Blog with Gatsby&lt;/h2&gt;
&lt;p&gt;Start with Gatsby’s starter template:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; init gatsby
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; blog
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run develop&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This gives you hot reloading and a skeleton site. I then customized it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added a terminal-inspired theme (CRT effects, scanlines, boot logs).&lt;/li&gt;
&lt;li&gt;Created Markdown-based blog posts inside &lt;code class=&quot;language-text&quot;&gt;/content/posts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added tags to frontmatter so I could filter posts later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the site worked locally, I ran:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gatsby build&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This outputs static HTML into &lt;code class=&quot;language-text&quot;&gt;/public&lt;/code&gt; — ready to be served.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-dockerizing-gatsby-output&quot;&gt;4. Dockerizing Gatsby Output&lt;/h2&gt;
&lt;p&gt;The goal: run Gatsby’s static HTML in &lt;strong&gt;Nginx inside a container&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here’s the &lt;code class=&quot;language-text&quot;&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; nginx:alpine&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; public /usr/share/nginx/html&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; nginx.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;nginx.conf&lt;/code&gt; ensures single-page app routing works correctly (important for 404 pages and client-side navigation):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;nginx&quot;&gt;&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server_name&lt;/span&gt; _&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;root&lt;/span&gt; /usr/share/nginx/html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;index&lt;/span&gt; index.html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; /&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;try_files&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$uri&lt;/span&gt; /index.html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then build for ARM64:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; buildx build &lt;span class=&quot;token parameter variable&quot;&gt;--platform&lt;/span&gt; linux/arm64 &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.1.66:32358/gatsby-blog:latest &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--push&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; buildx imagetools inspect &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.1.66:32358/gatsby-blog:latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This shows manifest lists (good for ARM64 compatibility).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-local-container-registry-on-the-pi&quot;&gt;5. Local Container Registry on the Pi&lt;/h2&gt;
&lt;p&gt;I didn’t want to push to Docker Hub, so I use the Pi as its own registry (via containerd + NodePort).&lt;/p&gt;
&lt;p&gt;After pushing, confirm the image is there:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ctr &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; k8s.io images &lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; gatsby-blog&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If it shows up, the cluster can pull it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: Add &lt;code class=&quot;language-text&quot;&gt;insecure-registries&lt;/code&gt; to your Docker config if you’re not using TLS on the Pi registry:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;insecure-registries&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;192.168.1.66:32358&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-deploying-with-helm&quot;&gt;6. Deploying with Helm&lt;/h2&gt;
&lt;p&gt;Instead of manually writing &lt;code class=&quot;language-text&quot;&gt;kubectl apply -f&lt;/code&gt;, I use Helm. My chart:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# values.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 192.168.1.66&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;32358/gatsby&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;blog
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; latest
  &lt;span class=&quot;token key atrule&quot;&gt;pullPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Always

&lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NodePort
  &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;nodePort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;32360&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;nginx&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog.chrishouse.io
      &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
          &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls
      &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; blog.chrishouse.io&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Deploy:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt; gatsby ./helm &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; blog --create-namespace   &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;image.repository&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.1.66:32358/gatsby-blog   &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;image.tag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes pulls from the Pi registry, starts a pod, and exposes it at NodePort 32360.&lt;/p&gt;
&lt;p&gt;Check it:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; blog get pods &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; wide
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; blog get svc gatsby-gatsby-blog&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-exposing-with-cloudflare-tunnel&quot;&gt;7. Exposing with Cloudflare Tunnel&lt;/h2&gt;
&lt;p&gt;I didn’t want to port forward. Enter &lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;tunnel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4eab82fd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;9172&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;4c0f&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;aaeb&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;237c72452dbe
&lt;span class=&quot;token key atrule&quot;&gt;credentials-file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /home/chris/.cloudflared/4eab82fd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;9172&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;4c0f&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;aaeb&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;237c72452dbe.json

&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog.chrishouse.io
    &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;32360&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http_status&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Install and run:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;cloudflared tunnel create blog
cloudflared tunnel route dns blog blog.chrishouse.io
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; cloudflared &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start cloudflared&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now requests to &lt;code class=&quot;language-text&quot;&gt;https://blog.chrishouse.io&lt;/code&gt; are routed securely through Cloudflare.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefit&lt;/strong&gt;: No exposed home IP, no router config, free TLS.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-automating-the-workflow&quot;&gt;8. Automating the Workflow&lt;/h2&gt;
&lt;p&gt;I don’t want to manually run 10 commands. So in &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt; I added:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;docker:build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;docker buildx build --platform linux/arm64 -t 192.168.1.66:32358/gatsby-blog:latest . --push&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;helm:deploy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;helm upgrade --install gatsby ./helm -n blog --create-namespace --set image.repository=192.168.1.66:32358/gatsby-blog --set image.tag=latest&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;deploy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm run build &amp;amp;&amp;amp; npm run docker:build &amp;amp;&amp;amp; npm run helm:deploy&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now it’s literally one command:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run deploy&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-lessons-learned-along-the-way&quot;&gt;9. Lessons Learned Along the Way&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Caching issues&lt;/strong&gt;: Sometimes pods don’t pull the latest image. &lt;code class=&quot;language-text&quot;&gt;kubectl rollout restart deploy gatsby-gatsby-blog&lt;/code&gt; fixes it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource limits&lt;/strong&gt;: The Pi can handle ~10 pods, but keep an eye on memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare TLS&lt;/strong&gt;: Don’t fight self-signed certs. Terminate TLS at Cloudflare edge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm DRY&lt;/strong&gt;: Extract common charts into a shared repo (&lt;code class=&quot;language-text&quot;&gt;my_helm&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logging&lt;/strong&gt;: Container logs are visible in &lt;code class=&quot;language-text&quot;&gt;kubectl logs&lt;/code&gt; and shipped to Elastic.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-future-plans&quot;&gt;10. Future Plans&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Add CI/CD via GitHub Actions to trigger &lt;code class=&quot;language-text&quot;&gt;npm run deploy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add Grafana dashboards on the same Pi (already running Prometheus).&lt;/li&gt;
&lt;li&gt;Expand posts and make &lt;code class=&quot;language-text&quot;&gt;/blog/:slug&lt;/code&gt; the default for navigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;With Gatsby, Docker, Helm, and Cloudflare Tunnel, I now have a full &lt;strong&gt;production-like pipeline&lt;/strong&gt; running on a Raspberry Pi.&lt;br&gt;
This setup is overkill for a personal blog — but that’s the point. It’s a playground to learn Kubernetes, CI/CD, and security practices in a safe, low-cost way.&lt;/p&gt;
&lt;p&gt;If a Pi can do it, so can your enterprise cluster.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Inside a Dev Container: How to Standardize .NET Development]]></title><description><![CDATA[How Dev Containers solve the 'it works on my machine' problem: standardizing .NET development with Docker, VS Code, and secure corporate setup (NuGet feeds, HTTPS certs, Zscaler).]]></description><link>https://chrishouse.io/inside-dev-container/</link><guid isPermaLink="false">https://chrishouse.io/inside-dev-container/</guid><pubDate>Tue, 29 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Spinning up a .NET application locally in a corporate environment often turns into an exercise in frustration. Developers deal with inconsistent SDK versions, corporate proxy certificates, authentication errors against private feeds, and missing tools. Instead of writing code, they spend hours configuring machines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dev Containers fix that.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dev Containers are Docker-based development environments defined using &lt;code class=&quot;language-text&quot;&gt;.devcontainer&lt;/code&gt; config files and launched via Visual Studio Code. By defining your development environment as code, Dev Containers let you version, automate, and standardize everything needed to get up and running — regardless of which machine or developer is involved.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Problem                          →  Solved by
---------------------------------------------------------------
Proxy certificate errors          Trusting Zscaler root certs
HTTPS dev environment             dotnet dev-certs https
NuGet feed authentication         dotnet nuget add source with a secure PAT
&quot;It works on my machine&quot; bugs     Identical container builds
Inconsistent SDKs and tools       Pinned versions in Docker image&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Whether onboarding new developers or debugging issues across machines, Dev Containers give us a consistent, secure, and versioned workspace — every time.&lt;/p&gt;
&lt;h2 id=&quot;the-dockerfile-building-a-reliable-net-environment&quot;&gt;The Dockerfile: Building a Reliable .NET Environment&lt;/h2&gt;
&lt;p&gt;We use Microsoft’s official .NET SDK 8.0 image as our base:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; mcr.microsoft.com/dotnet/sdk:8.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Private NuGet feed authentication&lt;/strong&gt;&lt;br&gt;
We use a Personal Access Token (PAT) to connect to Azure DevOps feeds securely. This is injected as a build argument and configured at build time:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; dotnet nuget add source &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_URL}&quot;&lt;/span&gt;     --name &lt;span class=&quot;token string&quot;&gt;&quot;AzureDevOps&quot;&lt;/span&gt;     --username &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_USER}&quot;&lt;/span&gt;     --password &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_PAT}&quot;&lt;/span&gt;     --store-password-in-clear-text&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Is &lt;code class=&quot;language-text&quot;&gt;--store-password-in-clear-text&lt;/code&gt; secure?&lt;/em&gt;&lt;br&gt;
This option is required when using &lt;code class=&quot;language-text&quot;&gt;dotnet nuget add source&lt;/code&gt; with credentials. While storing the password in plain text is not ideal for production containers, it is acceptable in temporary dev containers that are not distributed or reused. To enhance security, avoid checking in any secrets and limit access to the container environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zscaler root certificate installation&lt;/strong&gt;&lt;br&gt;
In a corporate environment with deep packet inspection (like Zscaler), HTTPS traffic can be intercepted unless root certificates are trusted. We add the cert manually:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; ZscalerRootCerts/ZscalerRootCertificate.crt /usr/local/share/ca-certificates/&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; update-ca-certificates&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Dev HTTPS certificate setup&lt;/strong&gt;&lt;br&gt;
We generate and trust the HTTPS developer certificate so ASP.NET Core can serve over HTTPS locally:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; dotnet dev-certs https --clean &amp;amp;&amp;amp;     dotnet dev-certs https --trust || true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Standard tooling&lt;/strong&gt;&lt;br&gt;
We install common tools (curl, git, vim, etc.) so the container can support debugging, scripting, and customization.&lt;/p&gt;
&lt;h3 id=&quot;complete-dockerfile&quot;&gt;Complete Dockerfile&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; mcr.microsoft.com/dotnet/sdk:8.0&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Accept build args&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ARG&lt;/span&gt; ADO_FEED_URL&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ARG&lt;/span&gt; ADO_FEED_USER&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ARG&lt;/span&gt; ADO_FEED_PAT&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Required ENV vars&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; DOTNET_USE_POLLING_FILE_WATCHER=1     ASPNETCORE_URLS=http://+:8080     DOTNET_RUNNING_IN_CONTAINER=true     NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Install tools&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; apt-get update &amp;amp;&amp;amp;     apt-get install -y --no-install-recommends     ca-certificates     curl     wget     gnupg     git     unzip     bash     vim &amp;amp;&amp;amp;     apt-get clean &amp;amp;&amp;amp;     rm -rf /var/lib/apt/lists/*&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Add Zscaler certs&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;ZscalerRootCerts/ZscalerRootCertificate.crt&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;/usr/local/share/ca-certificates/ZscalerRootCertificate.crt&quot;&lt;/span&gt;]&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;ZscalerRootCerts/ZscalerRootCertificate.crt&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;/etc/ssl/certs/ZscalerRootCertificate.crt&quot;&lt;/span&gt;]&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; update-ca-certificates&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Set up NuGet source securely&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; dotnet nuget add source &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_URL}&quot;&lt;/span&gt;     --name &lt;span class=&quot;token string&quot;&gt;&quot;AzureDevOps&quot;&lt;/span&gt;     --username &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_USER}&quot;&lt;/span&gt;     --password &lt;span class=&quot;token string&quot;&gt;&quot;${ADO_FEED_PAT}&quot;&lt;/span&gt;     --store-password-in-clear-text&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Install ASP.NET dev certs&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; dotnet dev-certs https --clean &amp;amp;&amp;amp;     dotnet dev-certs https --trust || true&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Final devcontainer setup&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /workspace&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 80&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 443&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;devcontainerjson-wiring-vs-code-into-the-container&quot;&gt;devcontainer.json: Wiring VS Code into the Container&lt;/h2&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;devcontainer.json&lt;/code&gt; file tells VS Code how to build and configure the container:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;dockerFile&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../Dockerfile&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;ADO_FEED_URL&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;ADO_FEED_USER&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;ADO_FEED_PAT&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;workspaceFolder&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/workspace&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;remoteUser&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;customizations&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;vscode&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;extensions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ms-dotnettools.csharp&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This defines the build args, mounts your local code folder into &lt;code class=&quot;language-text&quot;&gt;/workspace&lt;/code&gt;, sets up common VS Code extensions, and runs the container as root for ease of configuration.&lt;/p&gt;
&lt;h2 id=&quot;end-result&quot;&gt;End Result&lt;/h2&gt;
&lt;p&gt;Every developer on the team — regardless of their host OS or permissions — can run:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet restore
dotnet run&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…and everything works.&lt;/p&gt;
&lt;p&gt;There are no surprises around certificate trust, no more configuring private feeds manually, and no wasted time figuring out what’s missing from a machine. It just works, every time.&lt;/p&gt;
&lt;h2 id=&quot;-sample-folder-structure&quot;&gt;📁 Sample Folder Structure&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;MyDotNetApp/
├── .devcontainer/
│   ├── devcontainer.json
│   └── Dockerfile
│
├── ZscalerRootCerts/
│   └── ZscalerRootCertificate.crt
│
├── src/
│   └── MyDotNetApp/
│       ├── MyDotNetApp.csproj
│       ├── Program.cs
│       └── Startup.cs
│
├── MyDotNetApp.sln
└── README.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;how-to-use&quot;&gt;How to Use&lt;/h2&gt;
&lt;p&gt;If you’re new to Dev Containers, getting started is simple — especially in Visual Studio Code.&lt;/p&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Install Docker Desktop (or Podman + Docker CLI on Linux)&lt;/li&gt;
&lt;li&gt;Install Visual Studio Code&lt;/li&gt;
&lt;li&gt;Install the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&quot;&gt;Dev Containers extension&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;steps-to-use&quot;&gt;Steps to Use&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Clone your repository and make sure the &lt;code class=&quot;language-text&quot;&gt;.devcontainer&lt;/code&gt; folder exists.&lt;/li&gt;
&lt;li&gt;Open the repo in VS Code.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;F1&lt;/strong&gt; (or Ctrl+Shift+P) and run:&lt;br&gt;
&lt;code class=&quot;language-text&quot;&gt;Dev Containers: Reopen in Container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VS Code will build and launch the container, automatically installing dependencies, mounting your source code, and setting up extensions.&lt;/li&gt;
&lt;li&gt;Once the container opens, use the terminal or debugging features as you normally would. You’re now working inside the container.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;add-a-launchjson-to-launch-the-app&quot;&gt;Add a launch.json to launch the app&lt;/h3&gt;
&lt;p&gt;For .NET apps, add a &lt;code class=&quot;language-text&quot;&gt;.vscode/launch.json&lt;/code&gt; file to let VS Code run and debug the app inside the container:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0.2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;configurations&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.NET Core Launch (web)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;coreclr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;request&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;launch&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;preLaunchTask&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;program&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;${workspaceFolder}/bin/Debug/net8.0/MyDotNetApp.dll&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;cwd&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;${workspaceFolder}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;stopAtEntry&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;serverReadyAction&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;openExternally&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;pattern&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Now listening on: (https?://\\S+)&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;env&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;ASPNETCORE_ENVIRONMENT&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Development&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;sourceFileMap&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;/Views&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;${workspaceFolder}/Views&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Dev Containers have turned complex setup into a repeatable, version-controlled process. In enterprise environments, where security, repeatability, and private package feeds add complexity, Dev Containers let developers focus on building software instead of fixing environments.&lt;/p&gt;
&lt;p&gt;Learn more: &lt;a href=&quot;https://containers.dev&quot;&gt;https://containers.dev&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Converting MicroK8s Kube Prometheus Stack into a Helm Chart]]></title><description><![CDATA[A deep, command-heavy walkthrough to export the MicroK8s Observability stack, split the manifests, and rebuild them as a reusable, parameterized Helm chart with sensible values, helpers, and migration tips.]]></description><link>https://chrishouse.io/kube-prom-stack-to-helm/</link><guid isPermaLink="false">https://chrishouse.io/kube-prom-stack-to-helm/</guid><pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post shows how to &lt;strong&gt;export&lt;/strong&gt; the Prometheus/Grafana stack that MicroK8s deploys, then &lt;strong&gt;restructure&lt;/strong&gt; it into a &lt;strong&gt;Helm chart&lt;/strong&gt; you can version, reuse, and customize. We’ll dump the manifests with &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt;, split them by resource kind, add Helm templating, and ship the chart to an OCI registry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MicroK8s&lt;/strong&gt; installed and working (&lt;code class=&quot;language-text&quot;&gt;microk8s status&lt;/code&gt; shows running).&lt;/li&gt;
&lt;li&gt;You previously enabled the Observability addon (Prometheus/Grafana/etc.).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm 3+&lt;/strong&gt; installed.&lt;/li&gt;
&lt;li&gt;A shell with &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt;&lt;/strong&gt; (use &lt;code class=&quot;language-text&quot;&gt;microk8s kubectl&lt;/code&gt; if you don’t have a separate kubeconfig).&lt;/li&gt;
&lt;li&gt;Optional: &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;yq&lt;/code&gt;&lt;/strong&gt; (v4+) for YAML processing (makes splitting easier).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Confirm cluster access and the label we’ll filter on:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;microk8s status --wait-ready

&lt;span class=&quot;token comment&quot;&gt;# See the resources and confirm the instance label.&lt;/span&gt;
microk8s kubectl get all &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; app.kubernetes.io/instance&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;kube-prom-stack&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;If your addon uses a different label, adjust commands accordingly. Common variations include &lt;code class=&quot;language-text&quot;&gt;app=prometheus&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;app.kubernetes.io/part-of=kube-prometheus-stack&lt;/code&gt;, or a custom &lt;code class=&quot;language-text&quot;&gt;release&lt;/code&gt; label. The examples below use &lt;code class=&quot;language-text&quot;&gt;app.kubernetes.io/instance=kube-prom-stack&lt;/code&gt; throughout.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-1--dump-everything-to-a-single-yaml&quot;&gt;Step 1 — Dump everything to a single YAML&lt;/h2&gt;
&lt;p&gt;We’ll export &lt;strong&gt;all core workload types&lt;/strong&gt; plus config and storage objects so nothing is missed.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# One-shot dump (workloads + config + storage), across all namespaces&lt;/span&gt;
microk8s kubectl get   deploy,sts,ds,job,cronjob,svc,ep,ingress,cm,secret,sa,role,rolebinding,clusterrole,clusterrolebinding,pvc   &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; app.kubernetes.io/instance&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;kube-prom-stack &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml   &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; kube-prom-stack.dump.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Some stacks also install CRDs and CRD-backed resources such as &lt;code class=&quot;language-text&quot;&gt;ServiceMonitor&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;PodMonitor&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;PrometheusRule&lt;/code&gt;. Dump them too if present:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Optional but recommended (won&apos;t fail if kinds don&apos;t exist)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;kind&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; servicemonitor.monitoring.coreos.com podmonitor.monitoring.coreos.com prometheusrule.monitoring.coreos.com&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  microk8s kubectl get &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$kind&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; app.kubernetes.io/instance&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;kube-prom-stack &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; kube-prom-stack.dump.yaml &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why a single file first?&lt;/strong&gt; It’s easier to archive and review. We’ll split it in the next step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-2--split-the-dump-into-logical-files&quot;&gt;Step 2 — Split the dump into logical files&lt;/h2&gt;
&lt;p&gt;You can do this by hand, but &lt;code class=&quot;language-text&quot;&gt;yq&lt;/code&gt; + a few lines of shell makes it painless.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Make a staging folder&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; export-split

&lt;span class=&quot;token comment&quot;&gt;# Split the multi-doc YAML into numbered chunks&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;csplit&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; export-split/chunk- kube-prom-stack.dump.yaml &lt;span class=&quot;token string&quot;&gt;&apos;/^---$/&apos;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{*}&apos;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# For each chunk, detect kind/name/ns and write a smart filename&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; export-split/chunk-*&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token assign-left variable&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;yq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;.kind // &quot;&quot;&apos;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token assign-left variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;yq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;.metadata.name // &quot;&quot;&apos;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token assign-left variable&quot;&gt;ns&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;yq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;.metadata.namespace // &quot;default&quot;&apos;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$kind&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;continue&lt;/span&gt;
  &lt;span class=&quot;token assign-left variable&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;export-split/&lt;span class=&quot;token variable&quot;&gt;${ns}&lt;/span&gt;_&lt;span class=&quot;token variable&quot;&gt;${kind}&lt;/span&gt;_&lt;span class=&quot;token variable&quot;&gt;${name}&lt;/span&gt;.yaml&quot;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$out&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now you have a pile of files like &lt;code class=&quot;language-text&quot;&gt;monitoring_Deployment_prometheus.yaml&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;default_Service_grafana.yaml&lt;/code&gt;, etc. This is our source material for Helm templating.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-3--create-a-helm-chart-skeleton&quot;&gt;Step 3 — Create a Helm chart skeleton&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm create kube-prom-stack
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; kube-prom-stack

&lt;span class=&quot;token comment&quot;&gt;# Remove the example templates Helm generated; we’ll add our own&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; templates/*

&lt;span class=&quot;token comment&quot;&gt;# Add helpers for names/labels&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; templates/_helpers.tpl &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;EOF&apos;
{{/*
Expand the chart name.
*/}}
{{- define &quot;kube-prom-stack.name&quot; -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix &quot;-&quot; -}}
{{- end -}}

{{/*
Create a default fully qualified app name.
*/}}
{{- define &quot;kube-prom-stack.fullname&quot; -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix &quot;-&quot; -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf &quot;%s-%s&quot; .Release.Name $name | trunc 63 | trimSuffix &quot;-&quot; -}}
{{- end -}}
{{- end -}}

{{/*
Common labels
*/}}
{{- define &quot;kube-prom-stack.labels&quot; -}}
app.kubernetes.io/name: {{ include &quot;kube-prom-stack.name&quot; . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace &quot;+&quot; &quot;_&quot; }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
EOF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;The helpers consolidate name/label logic across templates so you don’t repeat yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-4--scaffold-valuesyaml-with-tunables&quot;&gt;Step 4 — Scaffold &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt; with tunables&lt;/h2&gt;
&lt;p&gt;We’ll define images, persistence, service types, and Grafana credentials in &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# values.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; monitoring

&lt;span class=&quot;token key atrule&quot;&gt;prometheus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prom/prometheus
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v2.54.0
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIP
    &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9090&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;storageClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; microk8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;hostpath
    &lt;span class=&quot;token key atrule&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20Gi

&lt;span class=&quot;token key atrule&quot;&gt;grafana&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grafana/grafana
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10.4.1&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIP
    &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;adminUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin
  &lt;span class=&quot;token key atrule&quot;&gt;adminPassword&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin123 &lt;span class=&quot;token comment&quot;&gt;# consider overriding via values or an existingSecret&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;alertmanager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prom/alertmanager
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v0.27.0
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIP
    &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9093&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;rbac&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;serviceMonitors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;prometheusRules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep credentials out of Git by using &lt;code class=&quot;language-text&quot;&gt;--set-file&lt;/code&gt; or a private &lt;code class=&quot;language-text&quot;&gt;values-prod.yaml&lt;/code&gt;. You can also support &lt;code class=&quot;language-text&quot;&gt;existingSecret&lt;/code&gt; patterns if you prefer K8s-managed secrets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-5--start-templating-the-core-deployments&quot;&gt;Step 5 — Start templating the core Deployments&lt;/h2&gt;
&lt;h3 id=&quot;prometheus-deployment&quot;&gt;Prometheus (Deployment)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/prometheus-deploy.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prometheus
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.prometheus.replicas &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prometheus
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prometheus
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prometheus
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prometheus
        &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ .Values.prometheus.image }}:{{ .Values.prometheus.tag }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--config.file=/etc/prometheus/prometheus.yml&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--storage.tsdb.path=/prometheus&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
            &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9090&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; data
            &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /prometheus
      &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; data
          &lt;span class=&quot;token key atrule&quot;&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;claimName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prometheus&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pvc&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We match on labels we control and keep PVC names deterministic via &lt;code class=&quot;language-text&quot;&gt;fullname&lt;/code&gt; + suffix.&lt;/p&gt;
&lt;h3 id=&quot;prometheus-service&quot;&gt;Prometheus Service&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/prometheus-svc.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prometheus
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.prometheus.service.type &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
      &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.prometheus.service.port &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prometheus
    &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Services should &lt;strong&gt;select by pod template labels&lt;/strong&gt;, not metadata labels, so scaling/restarts don’t break routing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;prometheus-pvc&quot;&gt;Prometheus PVC&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/prometheus-pvc.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; PersistentVolumeClaim
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prometheus&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pvc
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ReadWriteOnce&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.prometheus.persistence.size &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.prometheus.persistence.storageClass &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-6--grafana-deployment-service-secret-optional-ingress&quot;&gt;Step 6 — Grafana (Deployment, Service, Secret, optional Ingress)&lt;/h2&gt;
&lt;h3 id=&quot;secret-admin-creds&quot;&gt;Secret (admin creds)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/grafana-secret.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Secret
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;auth
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Opaque
&lt;span class=&quot;token key atrule&quot;&gt;stringData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;admin-user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.adminUser &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; quote &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;admin-password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.adminPassword &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; quote &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;In production, handle secrets via &lt;code class=&quot;language-text&quot;&gt;existingSecret&lt;/code&gt; or external secret managers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;deployment&quot;&gt;Deployment&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/grafana-deploy.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.replicas &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grafana
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grafana
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grafana
        &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ .Values.grafana.image }}:{{ .Values.grafana.tag }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GF_SECURITY_ADMIN_USER
            &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;auth
                &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;user
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GF_SECURITY_ADMIN_PASSWORD
            &lt;span class=&quot;token key atrule&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;secretKeyRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;auth
                &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password
        &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
            &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;service&quot;&gt;Service&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/grafana-svc.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.service.type &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
      &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.service.port &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grafana
    &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;optional-ingress&quot;&gt;Optional Ingress&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/grafana-ingress.yaml&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.grafana.ingress.enabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.grafana.ingress.className &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;kubernetes.io/ingress.class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.ingress.className &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; quote &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.grafana.ingress.className &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default nil &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; range .Values.grafana.ingress.hosts &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; quote &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; $ &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;grafana
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $.Values.grafana.service.port &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; toYaml .Values.grafana.ingress.tls &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-7--alertmanager-kube-state-metrics-node-exporter&quot;&gt;Step 7 — Alertmanager, kube-state-metrics, node-exporter&lt;/h2&gt;
&lt;p&gt;These follow the same pattern: &lt;strong&gt;Deployment/DaemonSet + Service + (optional) PVC/ConfigMaps&lt;/strong&gt;. For node-exporter, you’ll likely have a &lt;strong&gt;DaemonSet&lt;/strong&gt; with host mounts and privileged mode.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/node-exporter-ds.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DaemonSet
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; . &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;exporter
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default .Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;exporter
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;exporter
        &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;hostPID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;hostNetwork&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;exporter
        &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prom/node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;exporter&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;v1.8.2
        &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--path.rootfs=/host&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; host
            &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /host
            &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; host
          &lt;span class=&quot;token key atrule&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Directory&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Validate security context and host mounts against your environment and security policies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-8--crd-backed-resources-servicemonitor-podmonitor-prometheusrule&quot;&gt;Step 8 — CRD-backed resources (ServiceMonitor, PodMonitor, PrometheusRule)&lt;/h2&gt;
&lt;p&gt;If MicroK8s deployed the Prometheus Operator CRDs, you’ll see &lt;code class=&quot;language-text&quot;&gt;ServiceMonitor&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;PodMonitor&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;PrometheusRule&lt;/code&gt; objects. Keep them templated and togglable.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/servicemonitors.yaml&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.serviceMonitors.enabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; range $i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; $sm &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;= .Values.serviceMonitors.items &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default list &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; monitoring.coreos.com/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ServiceMonitor
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; $ &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $sm.name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $.Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default $.Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; toYaml $sm.spec &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 2 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then in &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt; you can define &lt;code class=&quot;language-text&quot;&gt;serviceMonitors.items&lt;/code&gt; as raw snippets you copy from your dump.&lt;/p&gt;
&lt;p&gt;Same for rules:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# templates/prometheusrules.yaml&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.prometheusRules.enabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; range $i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; $rule &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;= .Values.prometheusRules.items &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default list &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; monitoring.coreos.com/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; PrometheusRule
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include &quot;kube&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prom&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stack.fullname&quot; $ &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $rule.name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $.Values.global.namespace &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; default $.Release.Namespace &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; toYaml $rule.spec &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 2 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;This approach avoids hardcoding dozens of CRD resources in templates; you &lt;strong&gt;paste&lt;/strong&gt; them into &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt; and keep control with feature flags.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-9--install-safely-two-migration-options&quot;&gt;Step 9 — Install safely (two migration options)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Option A — New namespace (safest)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# create a fresh namespace&lt;/span&gt;
microk8s kubectl create ns monitoring

&lt;span class=&quot;token comment&quot;&gt;# install your chart there&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; kube-prom &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; monitoring ./kube-prom-stack

&lt;span class=&quot;token comment&quot;&gt;# verify&lt;/span&gt;
microk8s kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; monitoring&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Option B — Replace the addon in-place&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scale down or disable the addon first to avoid name collisions:
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;microk8s disable observability
&lt;span class=&quot;token comment&quot;&gt;# or delete only the labeled resources&lt;/span&gt;
microk8s kubectl delete all,cm,secret,sa,role,rolebinding,clusterrole,clusterrolebinding,pvc      &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; app.kubernetes.io/instance&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;kube-prom-stack&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;Install your Helm chart using the &lt;strong&gt;same names&lt;/strong&gt; (via &lt;code class=&quot;language-text&quot;&gt;fullnameOverride&lt;/code&gt; if needed) so dashboards and PVCs align.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Helm cannot “adopt” existing resources; either install into a clean namespace or delete the old ones first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-10--test--validate&quot;&gt;Step 10 — Test &amp;#x26; validate&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Port-forward Prometheus&lt;/span&gt;
microk8s kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; monitoring port-forward svc/kube-prom-kube-prom-stack-prometheus &lt;span class=&quot;token number&quot;&gt;9090&lt;/span&gt;:9090

&lt;span class=&quot;token comment&quot;&gt;# Port-forward Grafana&lt;/span&gt;
microk8s kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; monitoring port-forward svc/kube-prom-kube-prom-stack-grafana &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;:3000&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check targets, alert rules, and dashboards. Confirm PVCs are bound against your &lt;code class=&quot;language-text&quot;&gt;storageClass&lt;/code&gt; (MicroK8s typically uses &lt;code class=&quot;language-text&quot;&gt;microk8s-hostpath&lt;/code&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-11--package-and-publish-the-chart&quot;&gt;Step 11 — Package and publish the chart&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# from the chart root&lt;/span&gt;
helm lint
helm package &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# push to an OCI registry (example: Azure Container Registry)&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;HELM_EXPERIMENTAL_OCI&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
helm push ./kube-prom-stack-0.1.0.tgz oci://myacr.azurecr.io/helm&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;For GitOps, push the chart and a &lt;code class=&quot;language-text&quot;&gt;values-prod.yaml&lt;/code&gt; to your repo; let Argo CD or Flux manage the release lifecycle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pods won’t start / CrashLoopBackOff&lt;/strong&gt; — Check mismatched selectors (Service &lt;code class=&quot;language-text&quot;&gt;selector&lt;/code&gt; must match pod template labels). Verify volumes and security contexts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No dashboards/targets&lt;/strong&gt; — If the addon used CRD-backed resources, make sure you imported &lt;code class=&quot;language-text&quot;&gt;ServiceMonitor&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;PodMonitor&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;PrometheusRule&lt;/code&gt; definitions into &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt; or templates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PVC Pending&lt;/strong&gt; — StorageClass name typo or missing MicroK8s hostpath storage: &lt;code class=&quot;language-text&quot;&gt;microk8s enable storage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;403 listing targets&lt;/strong&gt; — Grafana/Prometheus RBAC scoped too narrowly; enable &lt;code class=&quot;language-text&quot;&gt;rbac.create&lt;/code&gt; or add specific ClusterRoles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name collisions&lt;/strong&gt; — You didn’t uninstall the addon before installing the chart with same names. Use a clean namespace or delete originals.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;appendix-a--quick-splitter-with-yq-only&quot;&gt;Appendix A — Quick splitter with &lt;code class=&quot;language-text&quot;&gt;yq&lt;/code&gt; only&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-euo&lt;/span&gt; pipefail
&lt;span class=&quot;token assign-left variable&quot;&gt;SRC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;kube-prom-stack.dump.yaml&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;OUT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;export-split&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$OUT&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;yq &lt;span class=&quot;token builtin class-name&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;select(documentIndex &gt;= 0) | length&apos;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SRC&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;BEGIN{n=0}/^---$/{n++}{print &gt; sprintf(&quot;%s/chunk-%04d.yaml&quot;,&quot;&apos;&lt;/span&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$OUT&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&apos;&quot;&lt;/span&gt;,n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; &quot;$SRC&quot;
for f in &quot;$OUT&quot;/chunk-*.yaml; do
  kind=$(yq -r &apos;&lt;/span&gt;.kind // empty&lt;span class=&quot;token string&quot;&gt;&apos; &quot;$f&quot;); [ -z &quot;$kind&quot; ] &amp;amp;&amp;amp; { rm -f &quot;$f&quot;; continue; }
  name=$(yq -r &apos;&lt;/span&gt;.metadata.name // &lt;span class=&quot;token string&quot;&gt;&quot;noname&quot;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; &quot;$f&quot;)
  ns=$(yq -r &apos;&lt;/span&gt;.metadata.namespace // &lt;span class=&quot;token string&quot;&gt;&quot;default&quot;&lt;/span&gt;&apos; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$OUT&lt;/span&gt;/&lt;span class=&quot;token variable&quot;&gt;${ns}&lt;/span&gt;_&lt;span class=&quot;token variable&quot;&gt;${kind}&lt;/span&gt;_&lt;span class=&quot;token variable&quot;&gt;${name}&lt;/span&gt;.yaml&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;appendix-b--example-tree-after-templating&quot;&gt;Appendix B — Example tree after templating&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;kube-prom-stack/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── _helpers.tpl
    ├── alertmanager-deploy.yaml
    ├── grafana-deploy.yaml
    ├── grafana-ingress.yaml
    ├── grafana-secret.yaml
    ├── grafana-svc.yaml
    ├── node-exporter-ds.yaml
    ├── prometheus-deploy.yaml
    ├── prometheus-pvc.yaml
    ├── prometheus-svc.yaml
    ├── prometheusrules.yaml
    └── servicemonitors.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;The MicroK8s addon is fantastic for &lt;strong&gt;quick starts&lt;/strong&gt;, but moving the stack into &lt;strong&gt;Helm&lt;/strong&gt; gives you repeatability and control. The recipe above keeps your exported resources intact while layering on the Helm features you actually need: sane naming, configurable values, and optional CRD resources controlled in one &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[From Docker Desktop to Podman Desktop — and Why Quadlets Led Me to Kubernetes]]></title><description><![CDATA[A deep dive into migrating from Docker Desktop to Podman Desktop, what I learned along the way, and how experimenting with Podman Quadlets eventually pushed me toward Kubernetes for orchestration.]]></description><link>https://chrishouse.io/docker-to-podman-to-k8s/</link><guid isPermaLink="false">https://chrishouse.io/docker-to-podman-to-k8s/</guid><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Like many developers, I started with &lt;strong&gt;Docker Desktop&lt;/strong&gt; because it “just worked.” But over time, the licensing changes, resource usage, and the need for better security led me to explore &lt;strong&gt;Podman Desktop&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This article is both a &lt;strong&gt;migration guide&lt;/strong&gt; and a &lt;strong&gt;story of discovery&lt;/strong&gt;: how I replaced Docker Desktop with Podman, experimented with &lt;strong&gt;Quadlets&lt;/strong&gt; for container management, and ultimately landed on &lt;strong&gt;Kubernetes (MicroK8s)&lt;/strong&gt; for orchestration.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-1--migrating-from-docker-desktop-to-podman-desktop&quot;&gt;Part 1 — Migrating from Docker Desktop to Podman Desktop&lt;/h2&gt;
&lt;h3 id=&quot;why-leave-docker-desktop&quot;&gt;Why leave Docker Desktop?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Licensing &amp;#x26; cost&lt;/strong&gt;: Docker Desktop moved to a paid licensing model for enterprises.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource usage&lt;/strong&gt;: On Windows/macOS, Docker runs a hidden VM; Podman Desktop is lighter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Podman supports &lt;strong&gt;rootless containers&lt;/strong&gt;, running as your own user.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCI-first&lt;/strong&gt;: Podman works with OCI registries like ACR out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my daily dev flow, Podman Desktop became a drop-in replacement.&lt;/p&gt;
&lt;h3 id=&quot;installing-podman-desktop&quot;&gt;Installing Podman Desktop&lt;/h3&gt;
&lt;p&gt;On macOS or Windows:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; podman-desktop
&lt;span class=&quot;token comment&quot;&gt;# or download from https://podman-desktop.io/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Initialize the Podman VM (“machine”):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; machine init
&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; machine start&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check status:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; info&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;building-and-running-containers&quot;&gt;Building and Running Containers&lt;/h3&gt;
&lt;p&gt;All your familiar Docker commands work, but with &lt;code class=&quot;language-text&quot;&gt;podman&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; build &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; myapp:dev &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:80 myapp:dev&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pushing to Azure Container Registry (ACR):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; login myazurecontainergistry.azurecr.io
&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; tag myapp:dev myazurecontainergistry.azurecr.io/myapp:dev
&lt;span class=&quot;token function&quot;&gt;podman&lt;/span&gt; push myazurecontainergistry.azurecr.io/myapp:dev&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Podman even includes a &lt;strong&gt;Docker socket compatibility layer&lt;/strong&gt;, so tools expecting &lt;code class=&quot;language-text&quot;&gt;docker.sock&lt;/code&gt; still work.&lt;/p&gt;
&lt;h3 id=&quot;podman-compose&quot;&gt;Podman Compose&lt;/h3&gt;
&lt;p&gt;For local dev stacks:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;podman-compose&lt;/span&gt; up&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This reads &lt;code class=&quot;language-text&quot;&gt;docker-compose.yml&lt;/code&gt; the same way as Docker. Perfect for multi-service Angular/.NET stacks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-2--discovering-podman-quadlets&quot;&gt;Part 2 — Discovering Podman Quadlets&lt;/h2&gt;
&lt;p&gt;At some point, I wanted containers to &lt;strong&gt;auto-start at boot&lt;/strong&gt; and be managed like system services. That’s where &lt;strong&gt;Podman Quadlets&lt;/strong&gt; come in.&lt;/p&gt;
&lt;p&gt;Quadlets let you describe containers in &lt;code class=&quot;language-text&quot;&gt;.container&lt;/code&gt; files that systemd understands.&lt;/p&gt;
&lt;p&gt;Example: &lt;code class=&quot;language-text&quot;&gt;myapp.container&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ini&quot;&gt;&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;My App Container&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;myazurecontainergistry.azurecr.io/myapp:latest&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;host&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/data:/app/data&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;AutoUpdate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;registry&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;default.target&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Deploy it:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; myapp.container /etc/containers/systemd/
systemctl &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--now&lt;/span&gt; myapp.service&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now the container runs at boot, restarts on failure, and updates when you push new tags.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros of Quadlets:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Great for &lt;strong&gt;single-node setups&lt;/strong&gt; (edge servers, dev machines).&lt;/li&gt;
&lt;li&gt;Integrates tightly with systemd.&lt;/li&gt;
&lt;li&gt;Simple config, no extra orchestrator needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No scheduling or scaling beyond one node.&lt;/li&gt;
&lt;li&gt;Updating multiple apps at once gets messy.&lt;/li&gt;
&lt;li&gt;Secrets/config management is manual.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quadlets gave me a taste of container-as-service, but also highlighted the limits.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-3--why-quadlets-pushed-me-to-kubernetes-microk8s&quot;&gt;Part 3 — Why Quadlets Pushed Me to Kubernetes (MicroK8s)&lt;/h2&gt;
&lt;p&gt;Running one or two containers with Quadlets is fine. But as soon as I had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple APIs&lt;/strong&gt; (an-application API, Identity, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis/RabbitMQ&lt;/strong&gt; backing services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ingress/TLS&lt;/strong&gt; requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent Volumes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…I realized I was re-implementing &lt;strong&gt;Kubernetes-lite&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;MicroK8s&lt;/strong&gt; — Canonical’s lightweight Kubernetes.&lt;/p&gt;
&lt;h3 id=&quot;installing-microk8s&quot;&gt;Installing MicroK8s&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; snap &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; microk8s &lt;span class=&quot;token parameter variable&quot;&gt;--classic&lt;/span&gt;
microk8s status --wait-ready&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Enable common addons:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;microk8s &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; dns storage ingress observability&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;deploying-with-helm&quot;&gt;Deploying with Helm&lt;/h3&gt;
&lt;p&gt;Instead of writing individual manifests by hand, I packaged my services with Helm charts.&lt;/p&gt;
&lt;p&gt;Example deployment (Helm values-driven):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
        &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; myazurecontainergistry.azurecr.io/an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;1.0.0
        &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /config
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; config
      &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; config
        &lt;span class=&quot;token key atrule&quot;&gt;configMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; an&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;application&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;config&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now I had &lt;strong&gt;scaling, rolling updates, TLS ingress&lt;/strong&gt;, and &lt;strong&gt;persistent volumes&lt;/strong&gt; handled by Kubernetes instead of by hand.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-4--comparing-the-approaches&quot;&gt;Part 4 — Comparing the Approaches&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Docker Desktop&lt;/th&gt;
&lt;th&gt;Podman Desktop&lt;/th&gt;
&lt;th&gt;Podman Quadlets&lt;/th&gt;
&lt;th&gt;MicroK8s (Kubernetes)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rootless security&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource usage&lt;/td&gt;
&lt;td&gt;Heavy&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-service dev stack&lt;/td&gt;
&lt;td&gt;✅ (Compose)&lt;/td&gt;
&lt;td&gt;✅ (Compose)&lt;/td&gt;
&lt;td&gt;⚠️ Manual&lt;/td&gt;
&lt;td&gt;✅ (Helm/Manifests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-start at boot&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ (Deployments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling/HA&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets/Config mgmt&lt;/td&gt;
&lt;td&gt;Basic env vars&lt;/td&gt;
&lt;td&gt;Basic env vars&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;✅ (ConfigMaps/Secrets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-node cluster&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Podman Desktop&lt;/strong&gt; is a great daily driver and replacement for Docker Desktop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quadlets&lt;/strong&gt; are perfect for edge devices or simple single-node setups.&lt;/li&gt;
&lt;li&gt;But as soon as you need scaling, persistence, or multiple microservices, &lt;strong&gt;Kubernetes is the natural next step&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me, the progression was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Docker Desktop → 2. Podman Desktop → 3. Podman Quadlets → 4. MicroK8s with Helm.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step taught me something new about containers, orchestration, and the trade-offs between &lt;strong&gt;simplicity&lt;/strong&gt; and &lt;strong&gt;power&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Written April 2025 — a reflection on my container journey and why Kubernetes ultimately won out, but not without Podman teaching me valuable lessons along the way.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Converting from npm to Bun]]></title><description><![CDATA[Step-by-step guide to migrate a Node.js project from npm to Bun for faster builds and dependency management.]]></description><link>https://chrishouse.io/notes/converting-to-bun/</link><guid isPermaLink="false">https://chrishouse.io/notes/converting-to-bun/</guid><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Complete guide to converting an npm-based project to Bun, including Docker, CI/CD, and deployment configurations.&lt;/p&gt;
&lt;h2 id=&quot;why-bun&quot;&gt;Why Bun?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;10-100x faster&lt;/strong&gt; package installation than npm&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop-in replacement&lt;/strong&gt; for Node.js - no code changes needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in bundler&lt;/strong&gt; and test runner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller Docker images&lt;/strong&gt; when using Bun base image&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Existing project using npm&lt;/li&gt;
&lt;li&gt;Admin/sudo access (for Bun installation)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;step-1-install-bun&quot;&gt;Step 1: Install Bun&lt;/h2&gt;
&lt;h3 id=&quot;windows-powershell&quot;&gt;Windows (PowerShell)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;irm&lt;/span&gt; bun&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sh/install&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ps1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;iex&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;macoslinux&quot;&gt;macOS/Linux&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://bun.sh/install &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Verify installation:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;bun &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-2-install-dependencies-with-bun&quot;&gt;Step 2: Install Dependencies with Bun&lt;/h2&gt;
&lt;p&gt;In your project directory:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Remove npm lock file (optional but recommended)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; package-lock.json

&lt;span class=&quot;token comment&quot;&gt;# Install dependencies with Bun&lt;/span&gt;
bun &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This creates a &lt;code class=&quot;language-text&quot;&gt;bun.lock&lt;/code&gt; file (Bun&apos;s lockfile) and installs all packages from &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt; stays the same - no changes needed!&lt;/p&gt;
&lt;h2 id=&quot;step-3-test-your-build&quot;&gt;Step 3: Test Your Build&lt;/h2&gt;
&lt;p&gt;Verify everything works:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;bun run build
bun run dev
bun run &lt;span class=&quot;token builtin class-name&quot;&gt;test&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;All npm scripts work as-is with &lt;code class=&quot;language-text&quot;&gt;bun run&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;step-4-update-dockerfile&quot;&gt;Step 4: Update Dockerfile&lt;/h2&gt;
&lt;p&gt;If you use Docker, update your Dockerfile to use Bun:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before (npm):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:20-slim &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; builder&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package*.json ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm install&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm run build&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;After (Bun):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; oven/bun:1 &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; builder&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package.json bun.lock ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; bun install --frozen-lockfile&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; bun run build&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Key changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base image: &lt;code class=&quot;language-text&quot;&gt;oven/bun:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy &lt;code class=&quot;language-text&quot;&gt;bun.lock&lt;/code&gt; instead of &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;code class=&quot;language-text&quot;&gt;bun install --frozen-lockfile&lt;/code&gt; (like &lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;step-5-update-cicd-pipeline&quot;&gt;Step 5: Update CI/CD Pipeline&lt;/h2&gt;
&lt;h3 id=&quot;github-actions&quot;&gt;GitHub Actions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Set up Node.js
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;20&apos;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm install

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run build&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Set up Bun
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; oven&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;sh/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;bun@v2
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;bun-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; latest

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bun install &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;frozen&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lockfile

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bun run build&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;cloudflare-workerspages&quot;&gt;Cloudflare Workers/Pages&lt;/h3&gt;
&lt;p&gt;No changes needed! Wrangler works with Bun out of the box.&lt;/p&gt;
&lt;p&gt;Just update your npm scripts:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;deploy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bun run build &amp;amp;&amp;amp; wrangler pages deploy public&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-6-update-gitignore&quot;&gt;Step 6: Update .gitignore&lt;/h2&gt;
&lt;p&gt;Add Bun-specific ignores:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;gitignore&quot;&gt;&lt;pre class=&quot;language-gitignore&quot;&gt;&lt;code class=&quot;language-gitignore&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Dependencies&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;node_modules&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Lock files (choose one approach)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Option 1: Keep both for compatibility&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;package-lock.json&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;yarn.lock&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Option 2: Bun only (remove npm lock from git)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# package-lock.json committed previously will be ignored&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;bun.lock&lt;/code&gt; should be committed to git (just like &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id=&quot;common-commands&quot;&gt;Common Commands&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Install dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm install &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun add &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm install -D &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun add -d &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add dev dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm uninstall &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun remove &amp;lt;pkg&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm run &amp;lt;script&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun run &amp;lt;script&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run package.json script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npx &amp;lt;command&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bunx &amp;lt;command&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Execute package binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bun install --frozen-lockfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean install&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;verification-checklist&quot;&gt;Verification Checklist&lt;/h2&gt;
&lt;p&gt;After conversion, verify:&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;bun install&lt;/code&gt; completes successfully&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;bun run build&lt;/code&gt; works&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;bun run dev&lt;/code&gt; starts dev server&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; All tests pass with &lt;code class=&quot;language-text&quot;&gt;bun run test&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Docker build succeeds (if applicable)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; CI/CD pipeline passes&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Production deployment works&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;h3 id=&quot;peer-dependency-warnings&quot;&gt;Peer Dependency Warnings&lt;/h3&gt;
&lt;p&gt;Bun may show peer dependency warnings. These are usually safe to ignore:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;warn: incorrect peer dependency &quot;react@18.3.1&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;native-modules&quot;&gt;Native Modules&lt;/h3&gt;
&lt;p&gt;Most native modules work. If you encounter issues:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;bun &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--force&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;postinstall-scripts&quot;&gt;Postinstall Scripts&lt;/h3&gt;
&lt;p&gt;Some packages have postinstall scripts. Check with:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;bun pm untrusted&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run blocked postinstall scripts:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;bun pm trust &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;package-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;performance-comparison&quot;&gt;Performance Comparison&lt;/h2&gt;
&lt;p&gt;Real-world example (this blog):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fresh install&lt;/td&gt;
&lt;td&gt;~60s&lt;/td&gt;
&lt;td&gt;~5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install with cache&lt;/td&gt;
&lt;td&gt;~15s&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.5x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatsby build&lt;/td&gt;
&lt;td&gt;~210s&lt;/td&gt;
&lt;td&gt;~207s&lt;/td&gt;
&lt;td&gt;Similar*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev server start&lt;/td&gt;
&lt;td&gt;~15s&lt;/td&gt;
&lt;td&gt;~12s&lt;/td&gt;
&lt;td&gt;~20% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;*Gatsby uses its own build system (Webpack), so build time is similar. Projects using Bun&apos;s native bundler see much bigger speedups (2-10x faster).&lt;/p&gt;
&lt;h2 id=&quot;rollback-plan&quot;&gt;Rollback Plan&lt;/h2&gt;
&lt;p&gt;If you need to rollback:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Restore &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt; from git&lt;/li&gt;
&lt;li&gt;Delete &lt;code class=&quot;language-text&quot;&gt;bun.lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Revert Dockerfile and CI/CD changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs&quot;&gt;Bun Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/runtime/nodejs-apis&quot;&gt;Bun vs Node.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oven-sh/setup-bun&quot;&gt;GitHub Actions Bun Setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;notes&quot;&gt;Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bun is &lt;strong&gt;API-compatible&lt;/strong&gt; with Node.js - no code changes needed&lt;/li&gt;
&lt;li&gt;Works with all major frameworks (Next.js, Gatsby, Vite, etc.)&lt;/li&gt;
&lt;li&gt;Can run &lt;code class=&quot;language-text&quot;&gt;.js&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.ts&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.jsx&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.tsx&lt;/code&gt; files directly&lt;/li&gt;
&lt;li&gt;Not all npm packages are tested with Bun - check compatibility for critical dependencies&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[AKS MCP Server Setup]]></title><description><![CDATA[How to set up the Azure Kubernetes Service MCP server for Claude Code.]]></description><link>https://chrishouse.io/notes/aks-mcp-setup/</link><guid isPermaLink="false">https://chrishouse.io/notes/aks-mcp-setup/</guid><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Step-by-step setup for the AKS MCP server with Claude Code in VS Code.&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VS Code with Claude Code extension&lt;/li&gt;
&lt;li&gt;Azure CLI logged in (&lt;code class=&quot;language-text&quot;&gt;az login&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Kubernetes extension for VS Code (installs the AKS MCP binary)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;1-install-the-kubernetes-extension&quot;&gt;1. Install the Kubernetes Extension&lt;/h2&gt;
&lt;p&gt;Install the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools&quot;&gt;Kubernetes extension&lt;/a&gt; from the VS Code marketplace. This bundles the AKS MCP server binary.&lt;/p&gt;
&lt;p&gt;The binary gets installed to:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;C:\Users\&amp;lt;user&gt;\.vs-kubernetes\tools\aks-mcp\v0.0.11\aks-mcp.exe&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;2-create-mcpjson-in-your-repo&quot;&gt;2. Create &lt;code class=&quot;language-text&quot;&gt;.mcp.json&lt;/code&gt; in Your Repo&lt;/h2&gt;
&lt;p&gt;Create a &lt;code class=&quot;language-text&quot;&gt;.mcp.json&lt;/code&gt; file in your project root:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;aks-mcp&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;C:\\Users\\&amp;lt;user&gt;\\.vs-kubernetes\\tools\\aks-mcp\\v0.0.11\\aks-mcp.exe&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--transport&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--access-level&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;readwrite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;env&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Update the path to match your username.&lt;/p&gt;
&lt;h2 id=&quot;3-verify-connection&quot;&gt;3. Verify Connection&lt;/h2&gt;
&lt;p&gt;Restart Claude Code. You should see the MCP server connected in the status.&lt;/p&gt;
&lt;p&gt;Test with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;list my aks clusters&quot;&lt;/li&gt;
&lt;li&gt;&quot;show cluster status&quot;&lt;/li&gt;
&lt;li&gt;&quot;get pods in default namespace&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-you-can-do&quot;&gt;What You Can Do&lt;/h2&gt;
&lt;p&gt;The AKS MCP gives Claude access to:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Operations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clusters&lt;/td&gt;
&lt;td&gt;list, show, start, stop, scale, upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node pools&lt;/td&gt;
&lt;td&gt;list, add, delete, scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubectl&lt;/td&gt;
&lt;td&gt;get, describe, logs, exec, apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;metrics, events, diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking&lt;/td&gt;
&lt;td&gt;vnet, nsg, load balancer info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;access-levels&quot;&gt;Access Levels&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;readonly&lt;/code&gt; - Safe for production, only read operations&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;readwrite&lt;/code&gt; - Full access including mutations&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;notes&quot;&gt;Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MCP servers run locally - no cloud-hosted option yet&lt;/li&gt;
&lt;li&gt;The binary is bundled with the K8s extension, not a separate install&lt;/li&gt;
&lt;li&gt;Works with any kubeconfig context you have configured&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[VS Code Agent Skills Setup]]></title><description><![CDATA[How to create Agent Skills for VS Code Copilot and Claude Code.]]></description><link>https://chrishouse.io/notes/vscode-agent-skills/</link><guid isPermaLink="false">https://chrishouse.io/notes/vscode-agent-skills/</guid><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Agent Skills is an open standard from Anthropic for defining reusable instructions that AI coding assistants can load automatically.&lt;/p&gt;
&lt;h2 id=&quot;the-standard&quot;&gt;The Standard&lt;/h2&gt;
&lt;p&gt;Skills live in &lt;code class=&quot;language-text&quot;&gt;.github/skills/&amp;lt;skill-name&gt;/SKILL.md&lt;/code&gt; with YAML frontmatter:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;skill
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; When to use this skill
&lt;span class=&quot;token key atrule&quot;&gt;license&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MIT
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; username
  &lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Skill Title&lt;/span&gt;

Instructions, rules, and context for the AI...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;skills-created&quot;&gt;Skills Created&lt;/h2&gt;
&lt;h3 id=&quot;1-aks-cluster-management&quot;&gt;1. aks-cluster-management&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;.github/skills/aks-cluster-management/SKILL.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Start, stop, and manage AKS clusters for cost optimization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contains:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cluster inventory (names, resource groups, VM sizes)&lt;/li&gt;
&lt;li&gt;Start/stop commands for all clusters&lt;/li&gt;
&lt;li&gt;Crossplane webhook cleanup before stopping hub&lt;/li&gt;
&lt;li&gt;Cost information&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-arm-portal-governance&quot;&gt;2. arm-portal-governance&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;.github/skills/arm-portal-governance/SKILL.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Rules and constraints for GitOps infrastructure changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contains:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Critical rules (never/always do)&lt;/li&gt;
&lt;li&gt;Architecture overview&lt;/li&gt;
&lt;li&gt;Provisioning flow diagram&lt;/li&gt;
&lt;li&gt;Environment approval requirements&lt;/li&gt;
&lt;li&gt;Repository structure&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;creating-new-skills&quot;&gt;Creating New Skills&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create &lt;code class=&quot;language-text&quot;&gt;.github/skills/&amp;lt;name&gt;/SKILL.md&lt;/code&gt; with frontmatter&lt;/li&gt;
&lt;li&gt;Add the skill content&lt;/li&gt;
&lt;li&gt;Copy the content into &lt;code class=&quot;language-text&quot;&gt;.claude/CLAUDE.md&lt;/code&gt; for Claude Code compatibility&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://agentskills.io&quot;&gt;agentskills.io&lt;/a&gt; - The spec&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com&quot;&gt;Anthropic announcement&lt;/a&gt; - Agent Skills in VS Code&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Configuring CI/CD with Azure Pipelines and Nx Monorepos]]></title><description><![CDATA[How we configured Azure Pipelines to run Nx-optimized CI and trigger application-specific CD pipelines for a large Angular monorepo.]]></description><link>https://chrishouse.io/ci-cd-azure-nx/</link><guid isPermaLink="false">https://chrishouse.io/ci-cd-azure-nx/</guid><pubDate>Tue, 14 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When managing &lt;strong&gt;15+ Angular applications&lt;/strong&gt; in one product, the overhead of separate &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt; files, builds, and pipelines quickly becomes painful. To address this, we turned to &lt;strong&gt;Nx&lt;/strong&gt;, which provides tooling for managing monorepos with &lt;strong&gt;shared node_modules&lt;/strong&gt;, optimized dependency graphs, and powerful &lt;code class=&quot;language-text&quot;&gt;affected&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;This post details how I set up &lt;strong&gt;CI/CD pipelines in Azure DevOps&lt;/strong&gt; for our Nx monorepo — including how to detect changed apps, run only the relevant builds/tests, and trigger releases per micro-application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-nx&quot;&gt;Why Nx?&lt;/h2&gt;
&lt;p&gt;Nx lets you manage a large Angular monorepo with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;single package.json&lt;/strong&gt; for dependencies.&lt;/li&gt;
&lt;li&gt;Dependency-aware commands like &lt;code class=&quot;language-text&quot;&gt;nx affected:build&lt;/code&gt;, which only build/test/lint the projects impacted by a given change.&lt;/li&gt;
&lt;li&gt;Consistency across builds: every app is built with the same toolchain and caching strategy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The challenge was integrating this into &lt;strong&gt;Azure Pipelines&lt;/strong&gt; for both CI (build/test) and CD (deployments).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;two-pipeline-yamls-ci-and-reusable-template&quot;&gt;Two Pipeline YAMLs: CI and Reusable Template&lt;/h2&gt;
&lt;p&gt;We ended up with two main YAML files:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CI wrapper pipeline&lt;/strong&gt;&lt;br&gt;
Detects what apps were changed in a commit and sets pipeline variables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusable template&lt;/strong&gt;&lt;br&gt;
Executes &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;nx lint&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;nx build&lt;/code&gt;, and &lt;code class=&quot;language-text&quot;&gt;nx test&lt;/code&gt; for each app in a consistent, parameterized way.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-1--detect-changed-applications&quot;&gt;Step 1 — Detect Changed Applications&lt;/h2&gt;
&lt;p&gt;The first job, &lt;code class=&quot;language-text&quot;&gt;Get_Affected_App&lt;/code&gt;, scans the changeset paths and sets pipeline variables accordingly.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$path&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$changesFolder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$path&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-match&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/apps/app1-dashboard&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;##vso[task.setvariable variable=App1_Dashboard;isOutput=true]&lt;span class=&quot;token boolean&quot;&gt;$True&lt;/span&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;##vso[task.setvariable variable=NPM_Install;isOutput=true]&lt;span class=&quot;token boolean&quot;&gt;$True&lt;/span&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If any file under &lt;code class=&quot;language-text&quot;&gt;apps/app1-dashboard&lt;/code&gt; is modified, the variable &lt;code class=&quot;language-text&quot;&gt;App1_Dashboard=true&lt;/code&gt; is set.&lt;/li&gt;
&lt;li&gt;The variable &lt;code class=&quot;language-text&quot;&gt;NPM_Install=true&lt;/code&gt; ensures &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt; runs for the entire repo.&lt;/li&gt;
&lt;li&gt;This is repeated for each app in the monorepo (&lt;code class=&quot;language-text&quot;&gt;app2-dashboard&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;app3-dashboard&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This mechanism ensures that we only queue CI jobs for the applications that actually changed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-2--run-npm-install-once&quot;&gt;Step 2 — Run NPM Install Once&lt;/h2&gt;
&lt;p&gt;Because Nx shares a single &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, we only need to run &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt; &lt;strong&gt;once per pipeline run&lt;/strong&gt;, not per app.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NPM_Install_For_All
  &lt;span class=&quot;token key atrule&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;vmImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ubuntu-latest&apos;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; eq(dependencies.Get_Affected_App.outputs&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;NX_PROJECT_VARIABLE.NPM_Install&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &apos;true&apos;)
  &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;checkout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; self
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NodeTool@0
      &lt;span class=&quot;token key atrule&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;versionSpec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;18.x&apos;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci
      &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Install Node modules&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: Use &lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt; instead of &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt; for reproducible builds, since it respects &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-3--build--test-affected-applications&quot;&gt;Step 3 — Build &amp;#x26; Test Affected Applications&lt;/h2&gt;
&lt;p&gt;Now we spin up one job &lt;strong&gt;per app&lt;/strong&gt;, gated by the variables set earlier.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; App1_Dashboard_CI
  &lt;span class=&quot;token key atrule&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Agent&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Pool
    &lt;span class=&quot;token key atrule&quot;&gt;demands&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Agent.OS &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;equals Windows_NT
  &lt;span class=&quot;token key atrule&quot;&gt;dependsOn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NPM_Install_For_All
  &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; eq(dependencies.Get_Affected_App.outputs&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;NX_PROJECT_VARIABLE.App1_Dashboard&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &apos;true&apos;)
  &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;checkout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; self
      &lt;span class=&quot;token key atrule&quot;&gt;persistCredentials&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;and&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;build.yml
      &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;app1-dashboard&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Runs only if &lt;strong&gt;App1_Dashboard=true&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Calls the reusable template with the &lt;code class=&quot;language-text&quot;&gt;app_name&lt;/code&gt; parameter.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-4--reusable-build-template&quot;&gt;Step 4 — Reusable Build Template&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;setup-and-build.yml&lt;/code&gt; is where the Nx magic lives:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;app_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      echo &quot;Linting ${{ parameters.app_name }}&quot;
      npx nx lint ${{ parameters.app_name }}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Lint&quot;&lt;/span&gt;

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      echo &quot;Building ${{ parameters.app_name }}&quot;
      npx nx build ${{ parameters.app_name }} --configuration=production&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Build&quot;&lt;/span&gt;

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      echo &quot;Testing ${{ parameters.app_name }}&quot;
      npx nx test ${{ parameters.app_name }} --ci --code-coverage&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Test&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each app is linted, built, and tested consistently.&lt;/li&gt;
&lt;li&gt;Using a template reduces duplication across 15+ apps.&lt;/li&gt;
&lt;li&gt;Adding new apps is as simple as copying a job block in the CI YAML and pointing it at this template.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-5--continuous-delivery-cd&quot;&gt;Step 5 — Continuous Delivery (CD)&lt;/h2&gt;
&lt;p&gt;After a successful CI build, the last few steps handle deployment prep:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Copy build artifacts&lt;/strong&gt; into a staging folder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag the build&lt;/strong&gt; in Azure DevOps with the app name (not a git tag).&lt;/li&gt;
&lt;li&gt;Trigger the &lt;strong&gt;release pipeline&lt;/strong&gt; for that app.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example tagging step:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; PowerShell@2
  &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tag Build&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;targetType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;inline&apos;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      Write-Host &quot;##vso[build.addbuildtag]${{ parameters.app_name }}&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;The release pipeline listens for builds tagged with &lt;code class=&quot;language-text&quot;&gt;app1-dashboard&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;app2-dashboard&lt;/code&gt;, etc., and deploys only that micro-application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-6--why-this-works-for-nx-monorepos&quot;&gt;Step 6 — Why This Works for Nx Monorepos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scalable&lt;/strong&gt;: Adding new apps is trivial — copy a job, change the &lt;code class=&quot;language-text&quot;&gt;app_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficient&lt;/strong&gt;: Only changed apps are built/tested, saving agent time and compute cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent&lt;/strong&gt;: Every build runs the same lint/build/test flow via the shared template.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Releasable&lt;/strong&gt;: Build tags cleanly map to release triggers per micro-app.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nx’s &lt;code class=&quot;language-text&quot;&gt;affected&lt;/code&gt; commands are powerful, but Azure DevOps variable passing required a little &lt;strong&gt;brute force scripting&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;CI/CD pipelines become far easier to maintain once you &lt;strong&gt;standardize on templates&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;build tags&lt;/strong&gt; instead of git tags gave us precise control over release triggers.&lt;/li&gt;
&lt;li&gt;There’s still room to improve: caching &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, leveraging &lt;code class=&quot;language-text&quot;&gt;nx cloud&lt;/code&gt;, and consolidating release templates.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Migrating to Nx with Azure Pipelines gave us a &lt;strong&gt;repeatable, optimized, and modular CI/CD pipeline&lt;/strong&gt; for a monorepo with over 15 Angular applications.&lt;/p&gt;
&lt;p&gt;While this setup may look verbose at first, it has paid off in speed, consistency, and maintainability.&lt;br&gt;
Future improvements will likely include &lt;strong&gt;Nx Cloud caching&lt;/strong&gt; and &lt;strong&gt;deployment previews&lt;/strong&gt; per pull request.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally written Dec 14, 2021 — updated with more detail and commentary.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[When to Use Angular's forRoot() Method — A Deep Dive]]></title><description><![CDATA[A complete, example-driven explanation of Angular's forRoot() convention: why it exists, how it prevents multiple service instances across lazy-loaded modules, and best practices.]]></description><link>https://chrishouse.io/angular-forroot/</link><guid isPermaLink="false">https://chrishouse.io/angular-forroot/</guid><pubDate>Mon, 06 Nov 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;strong&gt;forRoot() pattern&lt;/strong&gt; in Angular is one of those things you see in documentation for years before realizing its &lt;em&gt;real&lt;/em&gt; power.&lt;br&gt;
In my case, it solved a subtle but frustrating problem: ensuring &lt;strong&gt;singleton services&lt;/strong&gt; work correctly across both &lt;strong&gt;eager-loaded&lt;/strong&gt; and &lt;strong&gt;lazy-loaded&lt;/strong&gt; modules.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Angular’s dependency injection system creates new instances of providers at the module level.&lt;br&gt;
If you provide a service inside a &lt;strong&gt;shared module&lt;/strong&gt;, then &lt;strong&gt;lazy-loaded modules&lt;/strong&gt; that import that shared module will each get their &lt;em&gt;own&lt;/em&gt; instance of the service.&lt;/p&gt;
&lt;p&gt;This breaks the expectation that a service should be a &lt;strong&gt;singleton&lt;/strong&gt; across the entire app.&lt;/p&gt;
&lt;p&gt;For example, imagine a simple counter service:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Injectable &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@angular/core&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Injectable&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterService&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now consider a shared module that provides this service:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NgModule &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@angular/core&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; CounterService &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./counter.service&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;NgModule&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  providers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CounterService&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SharedModule&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you import &lt;code class=&quot;language-text&quot;&gt;SharedModule&lt;/code&gt; directly into both eager and lazy modules, each lazy module gets a &lt;strong&gt;new CounterService&lt;/strong&gt;.&lt;br&gt;
Your counter resets per module — not what you want.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;demo-of-the-problem&quot;&gt;Demo of the Problem&lt;/h2&gt;
&lt;p&gt;Example without &lt;code class=&quot;language-text&quot;&gt;forRoot&lt;/code&gt;:&lt;br&gt;
&lt;a href=&quot;https://stackblitz.com/edit/angular-no-for-root-72x3ht?file=app/lazy/lazy.component.ts&quot;&gt;StackBlitz Demo&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eager components share the counter.&lt;/li&gt;
&lt;li&gt;Lazy-loaded components &lt;strong&gt;do not&lt;/strong&gt; — they get isolated instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-forroot-solution&quot;&gt;The forRoot() Solution&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;forRoot() convention&lt;/strong&gt; is Angular’s way of saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This module is providing global, singleton services.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead of providing services directly, the module exposes a static method &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt; that returns a &lt;code class=&quot;language-text&quot;&gt;ModuleWithProviders&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;sharedmodulets&quot;&gt;shared.module.ts&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NgModule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ModuleWithProviders &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@angular/core&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; CounterService &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./counter.service&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;NgModule&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SharedModule&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;forRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleWithProviders&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;SharedModule&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ngModule&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; SharedModule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      providers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CounterService&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;appmodulets&quot;&gt;app.module.ts&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NgModule &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@angular/core&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BrowserModule &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@angular/platform-browser&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SharedModule &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./shared/shared.module&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; AppComponent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./app.component&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; EagerComponent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./eager.component&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; routing &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./app.routing&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;NgModule&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  imports&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    BrowserModule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    SharedModule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ registers singletons once&lt;/span&gt;
    routing
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  declarations&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;AppComponent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; EagerComponent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  bootstrap&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;AppComponent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AppModule&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;👉 Example with &lt;code class=&quot;language-text&quot;&gt;forRoot&lt;/code&gt;:&lt;br&gt;
&lt;a href=&quot;https://stackblitz.com/edit/angular-no-for-root-yfhkgz?file=app%2Fapp.module.ts&quot;&gt;StackBlitz Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now both eager and lazy-loaded modules share the &lt;strong&gt;same CounterService instance&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;best-practices&quot;&gt;Best Practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt; when your module &lt;strong&gt;provides services&lt;/strong&gt; that should be &lt;strong&gt;singletons&lt;/strong&gt; across the entire app.&lt;/li&gt;
&lt;li&gt;Use a separate &lt;code class=&quot;language-text&quot;&gt;SharedModule&lt;/code&gt; (without providers) for &lt;strong&gt;declarations like directives, pipes, and components&lt;/strong&gt;.&lt;br&gt;
Don’t mix global services and UI declarations in the same module.&lt;/li&gt;
&lt;li&gt;If you also need per-feature service instances, consider adding a &lt;strong&gt;forChild()&lt;/strong&gt; method (used in Angular Router and NgRx).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-does-this-work&quot;&gt;Why Does This Work?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Angular only calls &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt; once — in the root module.&lt;/li&gt;
&lt;li&gt;Lazy-loaded modules import &lt;code class=&quot;language-text&quot;&gt;SharedModule&lt;/code&gt; &lt;em&gt;without&lt;/em&gt; calling &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This ensures the service is provided once at the root injector, not per module.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Angular official guide: &lt;a href=&quot;https://angular.io/guide/ngmodule-faq#what-is-the-forroot-method&quot;&gt;NgModule FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AngularFirst: &lt;a href=&quot;http://angularfirst.com/the-ngmodule-forroot-convention/&quot;&gt;The NgModule forRoot convention&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NgRx docs (they use &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;forFeature()&lt;/code&gt; extensively).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The &lt;code class=&quot;language-text&quot;&gt;forRoot()&lt;/code&gt; convention is simple but powerful.&lt;br&gt;
It prevents the frustrating “multiple instance” bug that occurs with &lt;strong&gt;lazy-loaded modules&lt;/strong&gt;, and ensures that services like authentication, configuration, or in this case a &lt;strong&gt;counter service&lt;/strong&gt;, remain &lt;strong&gt;singletons across your entire app&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;First published in 2017. Still one of my most popular posts. Updated with richer explanations, StackBlitz demos, and best practices.&lt;/em&gt;&lt;/p&gt;</content:encoded></item></channel></rss>