<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.sixeyed.com/rss" rel="self" type="application/atom+xml" /><link href="https://blog.sixeyed.com/" rel="alternate" type="text/html" /><updated>2026-07-05T16:57:56+00:00</updated><id>https://blog.sixeyed.com/rss</id><title type="html">Elton’s Blog</title><subtitle>Notes from the field of freelance IT consultant and trainer Elton Stoneman -  15x Microsoft MVP, Docker Captain and author for Pluralsight and Manning.</subtitle><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><entry><title type="html">Build Medic: An Agent that Helps Builds Run Green</title><link href="https://blog.sixeyed.com/build-medic-self-healing-ci-agent/" rel="alternate" type="text/html" title="Build Medic: An Agent that Helps Builds Run Green" /><published>2026-07-05T15:00:00+00:00</published><updated>2026-07-05T15:00:00+00:00</updated><id>https://blog.sixeyed.com/build-medic-self-healing-ci-agent</id><content type="html" xml:base="https://blog.sixeyed.com/build-medic-self-healing-ci-agent/"><![CDATA[<p>In one of my projects we run a long integration test pipeline. About 1,400 tests run in a suite which would take 80 hours to run sequentially; the full run takes 70 minutes with aggressive parallelism. We shard the test suite and run multiple build runners in parallel. Each runner deploys its own server stack (Python API plus a couple of .NET backends plus SQL Server database), waits for the stack to become healthy and then starts running tests.</p>

<p>Build runners and the server components all run in the same Kubernetes cluster(s). The servers are Windows and the database is Linux so we run cross-platform clusters with Linux and Windows 2025 nodes. We get regular failures starting up a Windows pod, which effectively stops a runner using that shard from running any tests. In our current architecture we pre-batch the tests for each shard, so a failed server means a whole subset of tests fail (we have an alternative implementation where agents pull tests from a queue, but that’s for another post).</p>

<p>The failure modes differ between platforms: on the on-prem RKE2 cluster it’s containerd sandbox timeouts and DNS dropouts; on AKS it’s disks not attaching. The remedy is usually pretty basic - isolate the bad node for that pipeline run, and delete the pod so it gets scheduled somewhere else. We have watchdog processes doing that with scripts, but that only captures the known failure modes. I wanted to build something with some more intelligence that could actively debug and fix a wider range of issues.</p>

<p>A good case for a custom AI agent, but the team is understandably nervous about hosting an agent inside our Kubernetes clusters and giving it admin-style powers. If the agent had full <code class="language-plaintext highlighter-rouge">kubectl</code> access it could do anything, so what might it do by mistake? And would the tokens end up costing more than the compute to run the pipelines? So I built a proof-of-concept with guardrails on cost and capability, using a simple test system and GitHub Actions running on self-hosted runners in my own Kubernetes cluster:</p>

<p class="notice--info"><a href="https://github.com/sixeyed/build-medic">sixeyed/build-medic</a> - Build Medic, an autonomous agent that helps integration test suites run cleanly</p>

<p class="notice--info"><strong>TL;DR:</strong> Build Medic watches integration-test stacks come up in a Kubernetes build farm. If a stack gets stuck, level 1 remediation is code: a deterministic playbook reconciles known config drift. Level 2 remediation uses the Claude Agent SDK, running an episode to diagnose and repair with hard limits (max $0.50 in token spend and 12 turns). The agent can fix more failure modes, but if it can’t find a fix within its guardrails it escalates to Rocket.Chat with a root-cause analysis. Cost and safety guardrails are enforced in code.</p>

<h2 id="the-agents-brief">The agent’s brief</h2>

<p>Integration tests should be the most confidence-giving part of the build process. A green IT build is the best kind of testing: real components in a real environment, where tests execute the full path. But when the <em>environment</em> fails, it’s a lot of expensive noise. Build stages are red from failed stack deployments, which go green again when you re-run them manually. In our case that adds another 40 minutes to get a green build, and if there are genuine test failures that’s more time wasted.</p>

<p>This is the job description for Build Medic: watch the test stacks as they come up, fix the ones stuck <em>before</em> their tests run, and make sure a build result reflects the code under test - not random failures in the cluster.</p>

<p>(Yes, we shouldn’t have regular failures in the cluster just starting pods. But these look like genuine Windows OS issues, or failure points between the host compute service and containerd, or the host network service and Flannel, or maybe something in the VMWare virtual networking stack. We’re in the process of collating all the failure details so we can log some issues).</p>

<p>In the sample app, we can trigger a GitHub Actions run from the UI and select from a known set of failure modes (or none), and choose whether or not to use the Build Medic:</p>

<p alt="GitHub Actions workflow run list for a pipeline with a workflow_dispatch trigger. Runs are named by injection mode: mode 4 chaos-unrecoverable with medic on shows a red failure; mode 3 chaos-recoverable with medic on shows a green pass; the same mode 3 with medic off shows a red failure; mode 2 misconfig with medic on shows a green pass. The Run workflow panel shows a dropdown for the injection mode set to chaos-recoverable and a ticked checkbox labelled Use the build-medic."><img src="/content/images/2026/07/build-medic-gha.png" alt="GitHub Actions workflow runs showing the same integration build passing or failing depending on failure mode and whether the Build Medic is enabled" /></p>

<h2 id="the-architecture">The architecture</h2>

<p>The demo setup is a small build farm running on my Kubernetes cluster. GitHub Actions run the builds on self-hosted runners, using <a href="https://github.com/actions/actions-runner-controller">ARC</a> ephemeral runner pods. Every build gets its own namespace, and the test suite is sharded across four clients. Each client deploys its own dedicated stack (a FastAPI server and a Postgres database, both as single-pod StatefulSets) and runs its slice of the tests. A failed stack deployment only blocks its own client, which gives a nice property for the demo: fault one stack and the other three act as a live control group.</p>

<p>Build Medic itself is a single Python Deployment - one replica, stateless - which monitors all the builds in the cluster. Every five seconds it sweeps the namespaces labelled for watching, checks each stack’s health, and applies a debounce window so it ignores pods that are genuinely starting up. A stack that stays not-ready past the window - or hits a terminal state like <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code> - triggers a remediation <em>episode</em>.</p>

<p>The stacks use StatefulSets rather than Deployments to mimic the real IT suite. That means the test clients can address their stacks by stable DNS name and it’s easy to correlate logs between test clients and servers - we know client stage <code class="language-plaintext highlighter-rouge">1</code> for a build will use <code class="language-plaintext highlighter-rouge">server-0</code>, which will use <code class="language-plaintext highlighter-rouge">db-0</code>.</p>

<p>In the build workflow, test runners wait for their local stack to be ready before they start. The server’s readiness probe powers that, and it’s a genuine test of readiness: <code class="language-plaintext highlighter-rouge">/readyz</code> runs a real (short) database query and returns a 503 if the database is unreachable. A permanent failure between the server and the database, or the build runner and the server, means that stage just polls until a timeout.</p>

<h2 id="tier-1-fixing-the-easy-fixes-without-ai">Tier 1: fixing the easy fixes without AI</h2>

<p>Runaway token costs are the scary part of agent deployment. That’s mitigated here because we run the agent as a singleton. In the real pipeline there are seldom more than 5 builds running concurrently, which means about 300 pods to monitor so scale is modest.</p>

<p>And within the Build Medic, we avoid making any model calls until we need to. The Python logic has a playbook built in so it can fix known failure modes without handing off to an AI investigation. In the demo app we simulate real failures. Some are easy to find and fix, others take more work. One failure mode is to inject drift, so the deployed stack is not using the correct spec. Every stack is deployed with <em>declared-intent</em> annotations to capture what should be running:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">annotations</span><span class="pi">:</span>
  <span class="na">medic/expected-image</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">include "stack.expectedImage" . | quote</span> <span class="pi">}}</span>
  <span class="na">medic/expected-replicas</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.replicas | quote</span> <span class="pi">}}</span>
  <span class="na">medic/expected-command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>     <span class="c1"># empty = use image default; a set command is drift</span>
</code></pre></div></div>

<p>When a stack is stuck, tier 1 of the agent diffs the live spec against those annotations. If it finds drift (which is injected when the build runs with failure mode = <code class="language-plaintext highlighter-rouge">misconfig</code>), it applies a JSON Patch to reconcile it - fixing bad image tags, replicas scaled to zero, or failed command overrides. It’s a deterministic playbook and it’s fast and model-free. Known, catalogued failures - simulated here - are fixed silently with zero token spend. We save model usage for the cases that need investigation and judgement.</p>

<h2 id="tier-2-model-investigations-with-a-fixed-budget">Tier 2: model investigations with a fixed budget</h2>

<p>When tier 1 finds a problem outside of its playbook, the fault is more complex and we move to tier 2. Build Medic can run a custom agent built with the <a href="https://docs.claude.com/en/api/agent-sdk/overview">Claude Agent SDK</a>. The investigation is a headless session with a Claude model, where the agent sets its own context and gets its own list of tools. Each episode is an investigate - analyse - act loop over the available data: pod status, Kubernetes events, logs and nodes.</p>

<p>Failure mode <code class="language-plaintext highlighter-rouge">chaos-recoverable</code> simulates cluster failure by cordoning all the nodes. The stuck pod reports <code class="language-plaintext highlighter-rouge">node(s) were unschedulable</code> and the playbook doesn’t handle that. The agent has to reason its way there: check the pod events, query the nodes, discover the cordon, and then uncordon. That’s complex to encode in a simple playbook find-and-fix rule, but it’s straightforward for Claude.</p>

<p>The conversation is templated in <a href="https://github.com/sixeyed/build-medic/blob/main/agent/build_medic/agent.py">agent.py</a>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">prompt</span> <span class="o">=</span> <span class="p">(</span>
        <span class="sa">f</span><span class="s">"Stack for client </span><span class="si">{</span><span class="n">client_index</span><span class="si">}</span><span class="s"> in namespace </span><span class="si">{</span><span class="n">namespace</span><span class="si">}</span><span class="s"> is not "</span>
        <span class="sa">f</span><span class="s">"becoming ready. The StatefulSet is `</span><span class="si">{</span><span class="n">statefulset</span><span class="si">}</span><span class="s">` and its pod is "</span>
        <span class="sa">f</span><span class="s">"`</span><span class="si">{</span><span class="n">pod</span><span class="si">}</span><span class="s">`. Use the read-only tools (get_pod_events, describe_pod, "</span>
        <span class="sa">f</span><span class="s">"get_pod_logs, get_pod_status) on that exact pod name to diagnose the "</span>
        <span class="sa">f</span><span class="s">"root cause, then apply a single sanctioned remediation if one matches "</span>
        <span class="sa">f</span><span class="s">"(reconcile_to_intent / restart_pod / rollback_release on the "</span>
        <span class="sa">f</span><span class="s">"StatefulSet, or uncordon_node). Run id is </span><span class="si">{</span><span class="n">run_id</span><span class="si">}</span><span class="s">. If you cannot "</span>
        <span class="sa">f</span><span class="s">"durably fix it — or the fault keeps re-asserting — escalate exactly "</span>
        <span class="sa">f</span><span class="s">"once via the rocketchat tool with run id, namespace, client, stack, "</span>
        <span class="sa">f</span><span class="s">"reason, events summary, and the remediations you tried, then stop."</span>
        <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">link_hint</span><span class="si">}</span><span class="s">"</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>That’s a very specific prompt, and it’s coupled with an equally specific system prompt. We’re deliberately constraining what Claude can do - which adds a layer of safety at the cost of restricting its ability to investigate freely and fix unexpected issues. There’s a balance to find which you’ll get to through experimentation.</p>

<p>Model usage is also configured to enforce more constraints:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="n">ClaudeAgentOptions</span><span class="p">(</span>
    <span class="n">system_prompt</span><span class="o">=</span><span class="n">SYSTEM_PROMPT</span><span class="p">,</span>
    <span class="n">allowed_tools</span><span class="o">=</span><span class="n">ALLOWED_TOOLS</span><span class="p">,</span>               <span class="c1"># 10 named MCP tools only
</span>    <span class="n">disallowed_tools</span><span class="o">=</span><span class="n">_DISALLOWED_BUILTINS</span><span class="p">,</span>     <span class="c1"># Bash/Read/Write/WebFetch/...
</span>    <span class="n">setting_sources</span><span class="o">=</span><span class="p">[],</span>                        <span class="c1"># no host CLAUDE.md or skills
</span>    <span class="n">can_use_tool</span><span class="o">=</span><span class="n">_gate_writes</span><span class="p">(</span><span class="n">cfg</span><span class="p">),</span>            <span class="c1"># logs every write for audit
</span>    <span class="n">max_turns</span><span class="o">=</span><span class="n">cfg</span><span class="p">.</span><span class="n">max_turns</span><span class="p">,</span>                   <span class="c1"># 12
</span>    <span class="n">max_budget_usd</span><span class="o">=</span><span class="n">cfg</span><span class="p">.</span><span class="n">max_budget_usd</span><span class="p">,</span>         <span class="c1"># $0.50 per episode
</span>    <span class="n">model</span><span class="o">=</span><span class="n">cfg</span><span class="p">.</span><span class="n">model</span><span class="p">,</span>                           <span class="c1"># claude-sonnet-4-6
</span><span class="p">)</span>
</code></pre></div></div>

<p>Between the tight prompts and the agent config, we’re encoding guardrails to address both the cost and rogue-actor concerns.</p>

<h2 id="guardrail-1-token-costs">Guardrail 1: token costs</h2>

<p>The cost controls are layered, and each layer is a number you can point to in config:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Mechanism</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Free first</td>
      <td>Deterministic playbook handles catalogued drift</td>
      <td>$0.00</td>
    </tr>
    <tr>
      <td>Budget cap</td>
      <td><code class="language-plaintext highlighter-rouge">max_budget_usd</code> per episode, enforced by the SDK</td>
      <td>$0.50</td>
    </tr>
    <tr>
      <td>Turn cap</td>
      <td><code class="language-plaintext highlighter-rouge">max_turns</code> per episode - a misdiagnosis can’t loop</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Concurrency cap</td>
      <td>Semaphore bounds simultaneous episodes</td>
      <td>3</td>
    </tr>
    <tr>
      <td>Debounce</td>
      <td>No episode for pods that were coming up anyway</td>
      <td>20s</td>
    </tr>
    <tr>
      <td>Model choice</td>
      <td>Sonnet, not a bigger model, for a bounded diagnosis task</td>
      <td>-</td>
    </tr>
    <tr>
      <td>Off by default</td>
      <td>No API key → playbook-only mode, everything else escalates</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

<p>The worst case is fully computable: three concurrent episodes at fifty cents each is the most the agent can cost. This works if the real pipeline has environment failures in 30% of builds and we run max 10 builds concurrently. Builds take at least 70m so the maximum cost at full workload is $1.50 per hour. Every episode logs its actual cost from the SDK’s result message, so you can build up real numbers while you’re iterating.</p>

<p>I spent a few days on this project and ran 56 workflows - with a total agent cost of $2.80:</p>

<p alt="The Anthropic Console usage dashboard. Summary tiles show a total token cost of USD 2.80, with zero cost for web search, code execution and session runtime. A daily token cost bar chart covers June 29 to July 3, with each day's spend under one dollar, broken down into input, prompt caching write, prompt caching read and output tokens - prompt caching writes are the biggest share of each bar."><img src="/content/images/2026/07/build-medic-costs.png" alt="Anthropic Console usage dashboard showing the total token cost for the Build Medic project: USD 2.80" /></p>

<h2 id="guardrail-2-agent-capabilities">Guardrail 2: agent capabilities</h2>

<p>The rogue-actions concern is addressed in code too, at three layers: restricting what the agent can do, auditing what it actually does, and applying external access controls.</p>

<p>First - the tool layer. The agent doesn’t run a shell, and never gets raw <code class="language-plaintext highlighter-rouge">kubectl</code>. We explicitly list the tools it’s allowed to use, and they’re provided by custom MCP servers:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ALLOWED_TOOLS</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">"mcp__k8s__get_pod_status"</span><span class="p">,</span> <span class="s">"mcp__k8s__describe_pod"</span><span class="p">,</span>
    <span class="s">"mcp__k8s__get_pod_events"</span><span class="p">,</span> <span class="s">"mcp__k8s__get_pod_logs"</span><span class="p">,</span> <span class="s">"mcp__k8s__get_nodes"</span><span class="p">,</span>
    <span class="s">"mcp__k8s__reconcile_to_intent"</span><span class="p">,</span> <span class="s">"mcp__k8s__restart_pod"</span><span class="p">,</span>
    <span class="s">"mcp__k8s__rollback_release"</span><span class="p">,</span> <span class="s">"mcp__k8s__uncordon_node"</span><span class="p">,</span>
    <span class="s">"mcp__notify__rocketchat"</span><span class="p">,</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Five read paths for diagnosis, four write paths for explicit repair actions, one notification call. The SDK ships with all of Claude Code’s built-in tools - Bash, file access, web access, sub-tasks - and Build Medic explicitly disallows them all. We configure <code class="language-plaintext highlighter-rouge">setting_sources=[]</code> so the agent doesn’t load any host configuration. We constrain the world of the agent to our prompts and those ten tools.</p>

<p>The MCP servers are small and run in-process in the Build Medic service:</p>

<ul>
  <li><a href="https://github.com/sixeyed/build-medic/blob/main/agent/build_medic/mcp_k8s.py">mcp_k8s.py</a> - uses the Kubernetes client to provide limited access to the cluster: get pod events, restart pod etc.</li>
  <li><a href="https://github.com/sixeyed/build-medic/blob/main/agent/build_medic/mcp_notify.py">mcp_notify.py</a> - wraps a Rocket.Chat client so the agent can post escalation messages</li>
</ul>

<p>Next - the audit layer. Every write passes through a <code class="language-plaintext highlighter-rouge">can_use_tool</code> hook that logs the tool name and arguments, and each episode streams the full message log - diagnosis, tool calls, results, outcome. Every repair or failed attempt is explainable after the fact. When someone asks “did the agent uncordon that node?”, we can find the answer in the logs.</p>

<p>Finally - the platform layer. Even if the model went completely off the rails, we have Kubernetes RBAC to contain any damage. The agent pod runs with a specific ServiceAccount, with a ClusterRole that only grants the verbs needed for the MCP server. Most actions are specific to pods in the namespace for a specific build run, but uncordoning nodes affects the whole cluster so we gate that with a feature flag in the Helm chart:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{{</span><span class="nv">- if .Values.allowNodePatch</span> <span class="pi">}}</span>
  <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
    <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">nodes"</span><span class="pi">]</span>
    <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">watch"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">patch"</span><span class="pi">]</span>   <span class="c1"># patch = cordon/uncordon</span>
<span class="pi">{{</span><span class="nv">- else</span> <span class="pi">}}</span>
  <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
    <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">nodes"</span><span class="pi">]</span>
    <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">watch"</span><span class="pi">]</span>            <span class="c1"># no patch</span>
<span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p>Turn that flag off and the capability is gone, whatever the model decides to do. The system prompt also reinforces the boundaries - the agent must confirm a cordon via the node-listing tool before it uncordons anything. But prompts are guidance for the model and they might not be enforced, so a secondary safety measure like this is a good way of disabling actions in config without needing to stop or redeploy the Build Medic.</p>

<h2 id="demoing-failures-and-repairs">Demoing failures and repairs</h2>

<p>The GitHub action lets you run the same build with and without the agent to compare the results. The fault mode is a parameter, and a checkbox turns the medic on or off per run - it just labels the namespace, so you get baseline-versus-remediated comparisons without redeploying anything.</p>

<p>The failure mode + medic matrix proves out different scenarios:</p>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>Injected fault</th>
      <th>Without the medic</th>
      <th>With the medic</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">none</code></td>
      <td>Nothing</td>
      <td>Passes</td>
      <td>Passes - and the agent should do nothing, no Rocket.Chat post</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">misconfig</code></td>
      <td>One stack’s image patched to a bad tag after deploy</td>
      <td>That client fails</td>
      <td><strong>Tier 1</strong> reconciles the drift, rolls the pod - build passes, no LLM call</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">chaos-recoverable</code></td>
      <td>Nodes cordoned, target pod deleted</td>
      <td>Pod stuck <code class="language-plaintext highlighter-rouge">Pending</code>, client fails</td>
      <td><strong>Tier 2</strong> diagnoses the cordon, uncordons - build passes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">chaos-unrecoverable</code></td>
      <td>Sustained network fault between server and DB (Chaos Mesh)</td>
      <td>Client fails</td>
      <td>Agent diagnoses it, can’t remove it - <strong>escalates once</strong> with a root cause</td>
    </tr>
  </tbody>
</table>

<p>Mode <code class="language-plaintext highlighter-rouge">none</code> is the scenario you need to make sure you verify. An agent that “fixes” healthy environments is not good, so a clean run where the medic does nothing and there is no post in the chat discussion is a first-class test case.</p>

<p>The unrecoverable mode picks one of three network faults at random - partition, packet loss or delay - and the agent isn’t told which. It has to work it out from the signals, and the results land in Rocket.Chat, which is the same <a href="/monitor-ai-agents-with-rocket-chat/">agent chat channel pattern</a> I use for interactive Claude Code sessions:</p>

<p alt="Three Rocket.Chat messages posted by the Build Medic agent. The first two are green-tick resolutions titled build-medic remediated a stack, each linking to a GitHub Actions run and naming the namespace, client and StatefulSet: one lists uncordon_node actions, the other shows a bad image tag corrected from nope-does-not-exist to latest with the pod deleted, both ending the stack is Ready; integration tests can proceed. The third is a Build Medic Escalation titled Stack not ready, cannot durably fix, with run, namespace, client and stack details and a root cause: database unreachable, the readyz endpoint returns HTTP 503 persistently while healthz returns 200 OK, so the server process is healthy but cannot connect to its database dependency."><img src="/content/images/2026/07/build-medic-chat.png" alt="Rocket.Chat messages from Build Medic: two successful stack remediations and one escalation with a root-cause analysis" /></p>

<p>The escalation is more valuable than it looks. The agent couldn’t fix the network fault - the constraints stop it doing that - but it does the root cause analysis and posts the escalation while the build is still running. The test client runner is looping at this point, waiting for the server stack to get healthy. The chat notification can get picked up by the platform team, the diagnosis is already done and one of us has a chance to fix it and get the build back on track.</p>

<h2 id="next-steps">Next steps</h2>

<p>Don’t run my Build Medic in production. This is just an example of how you can engineer limitations around model calls to empirically answer the cost and danger concerns. Token costs are bounded by a free deterministic tier plus per-episode budget and turn caps; agent capability is bounded by a named tool allowlist, MCP servers and RBAC. None of that relies on the model behaving as expected - the guardrails prevent any side-effects from misalignment.</p>

<p>If anything, this example is too constrained. There are plenty of Chaos Mesh experiments which I’m sure Claude could find and remediate. The next steps would be to run a more YOLO-style agent in the QA environment and document the fixes it can reliably handle, and loosen the constraints to cover those scenarios too.</p>

<blockquote>
  <p>The code, charts and design docs are all in <a href="https://github.com/sixeyed/build-medic">github.com/sixeyed/build-medic</a> - it runs on a local k3d cluster if you want to break some stacks and watch them get fixed.</p>
</blockquote>

<h2 id="faq">FAQ</h2>

<h3 id="how-much-does-an-ai-build-repair-agent-cost-to-run">How much does an AI build-repair agent cost to run?</h3>

<p>Less than you’d think, if you design for it. In Build Medic the common failures are fixed by a deterministic playbook which makes no LLM calls at all - zero token cost. Only novel faults reach the model, and each episode is hard-capped at $0.50 and 12 turns, running on Sonnet rather than a bigger model. A misdiagnosis can’t loop and run up a bill, and a fix costs a few cents.</p>

<h3 id="how-do-you-stop-an-ai-agent-taking-dangerous-actions-in-a-kubernetes-cluster">How do you stop an AI agent taking dangerous actions in a Kubernetes cluster?</h3>

<p>Enforce the limits in code, not in the prompt. Build Medic’s agent never gets a shell or raw <code class="language-plaintext highlighter-rouge">kubectl</code> - it can only call a short list of named, audited operations (five read tools, four write tools, one notify tool). The Claude Code built-in tools are all disabled, every write is logged, and the Kubernetes RBAC role grants only the specific verbs those tools need, with the privileged node-patch permission behind a feature flag.</p>

<h3 id="what-happens-if-the-agent-cant-fix-the-environment">What happens if the agent can’t fix the environment?</h3>

<p>It escalates exactly once to a chat channel with a structured message - run ID, namespace, stack, root cause and the actions it tried - then stops working that stack. A deterministic backstop guarantees a broken stack never goes silent, and the escalate-once contract means it doesn’t spam the channel or thrash on a fault it can’t fix.</p>

<h3 id="wont-a-build-repair-agent-mask-real-failures-in-the-code">Won’t a build-repair agent mask real failures in the code?</h3>

<p>No - it only acts on environment faults before the tests run, never on test results. In the example the stacks are stamped with declared-intent annotations so the agent knows what the environment should look like, and repairs only reconcile back to that intent. One of the four demo pipeline modes injects no fault at all, and the pass condition there is that the agent takes no action.</p>

<h3 id="does-it-need-the-anthropic-api-or-can-it-run-inside-a-private-cloud-boundary">Does it need the Anthropic API, or can it run inside a private cloud boundary?</h3>

<p>The agent is built on the Claude Agent SDK, which can use the Anthropic API directly or target AWS Bedrock and Google Vertex AI, so inference can stay inside your cloud boundary. And without any API key configured the agent still runs - it just operates in playbook-only mode, fixing known drift deterministically and escalating everything else to the chat channel.</p>

<h3 id="why-not-just-retry-the-failed-build">Why not just retry the failed build?</h3>

<p>Retries hide the problem and pay for it twice - you burn a full build’s worth of compute just to try again, and the underlying fault (a cordoned node, a bad config push) is still there for the next build. Build Medic fixes the environment in-place while the build waits, so the run completes on the first attempt, and persistent faults get a root-cause escalation instead of an infinite retry loop.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="ai-agents" /><category term="claude" /><category term="claude-agent-sdk" /><category term="kubernetes" /><category term="github-actions" /><category term="ci-cd" /><category term="devops" /><category term="platform-engineering" /><summary type="html"><![CDATA[My team was nervous about an AI agent in our Kubernetes clusters - so I built a prototype with cost and safety guardrails in code, using the Claude Agent SDK.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2026/07/build-medic-architecture.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2026/07/build-medic-architecture.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Exploring Nscale’s Fleet Operations - in a Home Lab</title><link href="https://blog.sixeyed.com/nscale-fleet-operations-home-lab/" rel="alternate" type="text/html" title="Exploring Nscale’s Fleet Operations - in a Home Lab" /><published>2026-06-28T20:00:00+00:00</published><updated>2026-06-28T20:00:00+00:00</updated><id>https://blog.sixeyed.com/nscale-fleet-operations-home-lab</id><content type="html" xml:base="https://blog.sixeyed.com/nscale-fleet-operations-home-lab/"><![CDATA[<p><a href="https://www.nscale.com">Nscale</a> wrote up some interesting blog posts about Fleet Operations, their internal system for bringing GPU nodes online at scale. They use <a href="https://temporal.io">Temporal</a> to orchestrate the automation workflows, Slurm and Kubernetes for burn-in testing, and Grafana for observability. The posts walk through how a node goes from racked hardware to validated, customer-facing capacity:</p>

<ul>
  <li><a href="https://www.nscale.com/blog/fleet-operations">Fleet Operations</a></li>
  <li><a href="https://www.nscale.com/blog/lifecycle-of-a-node">The Lifecycle of a Node</a></li>
</ul>

<p>There are parts of the stack I’m very familiar with from production systems, plus some equivalents I’ve used at home (MAAS, OpenStack), and some I haven’t used at all (Temporal). It looked like a good learning opportunity, so I spent a couple of weeks with Claude researching and building out my own implementation which is here on GitHub:</p>

<p class="notice--info"><a href="https://github.com/sixeyed/fleet-lab">sixeyed/fleet-lab</a> - fleet provisioning for a home lab</p>

<p>What does it do? There’s a local k3d version you can run which stubs out the integration points but uses real Temporal workflows, so you can test the lifecycle of a fake node. The real version deploys to my local Kubernetes cluster and does the real thing: I can plug a new machine into the provisioning network and it gets automatically deployed, burn-in tested and enrolled, and ends up running an Nginx workload which I can switch out to Apache at a click.</p>

<p class="notice--info"><strong>TL;DR:</strong> I built an approximation of Nscale’s Fleet Operations node lifecycle as a home lab. The Python Fleet Manager drives the node lifecycle via Temporal workflows for a real-world <em>neocloud-in-your-own-home</em> experience. Plug in a box → MAAS PXE-provisions the OS and configures networking → Slurm runs a burn-in stress test → OpenStack enrols it as a compute host and launches a workload VM. The UI shows the lifecycle and all workflows and event logs.</p>

<p alt="Fleet Manager machine detail page for the node 'large-foal' at 10.50.0.251 with 4 CPU and 12 GB, MAAS status Deployed, showing a six-phase lifecycle bar (New, Commissioned, Deployed, Provisioned, Registered, Active), the node running the apache workload at 10.50.0.210, Reprovision and Recommission buttons, and a table of completed Temporal workflows (AllocateWorkload, DeallocateWorkload, ReallocateWorkload, RegisterCompute, BurnInMachine, DeployMachine, ProvisionMachine) each linking to the Temporal Web UI"><img src="/content/images/2026/06/fleet-nuc-01.png" alt="The Fleet Manager detail page for a node, showing its lifecycle progress and the Temporal workflows that drove it" /></p>

<h2 id="the-physical-architecture">The physical architecture</h2>

<p>If you want to try this out yourself, the big requirement is a network which you can partition into separate segments. The control plane runs on Kubernetes in your normal network range, but MAAS needs to own DHCP to allocate IP addresses for new machines - so you sit that in a separate range or it would interfere. And OpenStack also needs an IP range so it can allocate addresses for workloads. I use <a href="https://www.ui.com">UniFi</a> kit everywhere; the tiny <a href="https://techspecs.ui.com/unifi/switching/usw-flex-mini">USW-Flex-Mini</a> is a managed switch, so you can configure different segments for different ports. I have multiple NICs on my boxes (I use old Intel NUCs as the “fleet”):</p>

<p alt="Network diagram with three networks - the control-plane LAN on 192.168.2.0/24 running the Kubernetes cluster, and a flat VLAN 50 on 10.50.0.0/24 carrying a MAAS provisioning range (10.50.0.10-.199) and an OpenStack workload range (10.50.0.200-.240), with the fleet NUC connected by two NICs: eno1 for PXE and management, and a USB NIC as the OpenStack br-ex uplink"><img src="/content/images/2026/06/fleet-network-architecture.png" alt="Network diagram showing three networks: the control-plane LAN on 192.168.2.0/24 running the Kubernetes cluster, and a flat VLAN 50 on 10.50.0.0/24 split into a MAAS-owned provisioning range and an OpenStack workload range, with the fleet NUC connected by two NICs - eno1 for PXE and management, and a USB NIC as the OpenStack bridge uplink" /></p>

<p>So there are three network ranges. The control plane sits on my normal LAN (<code class="language-plaintext highlighter-rouge">192.168.2.0/24</code>) where the Kubernetes cluster runs. Everything else lives on a single flat VLAN 50 (<code class="language-plaintext highlighter-rouge">10.50.0.0/24</code>), which carries two ranges that don’t overlap: MAAS owns DHCP and hands out the provisioning addresses (<code class="language-plaintext highlighter-rouge">.10-.199</code>), and OpenStack allocates workload VMs from a reserved provider range (<code class="language-plaintext highlighter-rouge">.200-.240</code>). Each fleet NUC has two NICs on that VLAN - one for PXE and management, and a USB NIC is the uplink for OpenStack’s external bridge. The UniFi network routes between the LAN and VLAN 50.</p>

<p>The control plane is separate from the fleet - you don’t want your orchestration layer running on OpenStack alongside customer workloads, because if there’s an outage you lose your management layer too. In my lab, the control plane runs on an existing Kubernetes cluster on my LAN: Temporal and its Postgres database, the Fleet Manager app, the Slurm controller, and the power-shim. MAAS and OpenStack run on a separate machine which bridges the control plane and provisioning networks.</p>

<h2 id="mapping-a-data-centre-onto-two-nucs">Mapping a data centre onto two NUCs</h2>

<p>You don’t need exotic kit to run this, but there are parts of an industrial data centre you won’t have at home. Here’s how each piece of the lifecycle maps across - with my best guess at how Nscale runs it, pieced together from their blog posts and industry standards.</p>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Real data centre (Nscale)</th>
      <th>Home lab</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Compute nodes</td>
      <td>Racks of GPU servers</td>
      <td>2× Intel NUCs on my desk</td>
    </tr>
    <tr>
      <td>Power &amp; boot control</td>
      <td>Redfish on each node’s BMC</td>
      <td>Smart plug + power-shim API + one-time BIOS settings</td>
    </tr>
    <tr>
      <td>Bare-metal provisioning</td>
      <td>Autonomous PXE enrolment (Ironic / Metal³ / Tinkerbell?)</td>
      <td><a href="https://maas.io">MAAS</a></td>
    </tr>
    <tr>
      <td>Burn-in / validation</td>
      <td><code class="language-plaintext highlighter-rouge">gpu-burn</code> + DCGM via Slurm and Kubernetes</td>
      <td><a href="https://slurm.schedmd.com">Slurm</a> running <code class="language-plaintext highlighter-rouge">stress-ng</code> (CPU/memory/disk)</td>
    </tr>
    <tr>
      <td>Workload / capacity</td>
      <td>Validated node joins the scheduling pool (Kubernetes / OpenStack?)</td>
      <td><a href="https://www.openstack.org">OpenStack</a> - NUC enrolled as a <code class="language-plaintext highlighter-rouge">nova-compute</code> host</td>
    </tr>
    <tr>
      <td>Orchestration</td>
      <td>Temporal</td>
      <td>Temporal</td>
    </tr>
    <tr>
      <td>Observability</td>
      <td>Grafana - Mimir, Loki, Tempo</td>
      <td>Temporal Web UI + Fleet Manager UI</td>
    </tr>
    <tr>
      <td>Reclaim / remediation</td>
      <td>Automated workflows + Radar fault events</td>
      <td>UI-initiated <code class="language-plaintext highlighter-rouge">ReleaseMachine</code> / <code class="language-plaintext highlighter-rouge">ReprovisionMachine</code> workflows</td>
    </tr>
  </tbody>
</table>

<p>I don’t have racks of GPUs (those are all in my <a href="/mac-studio-llm-workstation/">Mac Studio boxes</a>), so the burn-in is just a quick CPU, memory and disk stress rather than <code class="language-plaintext highlighter-rouge">gpu-burn</code>. The NUCs don’t have BMC (the always-on remote management chip), so I can’t remotely tell a machine to reboot. Instead I control the mains with a smart plug and a simple REST API which MAAS can call to turn it on or off. Before provisioning I set each NUC’s BIOS to PXE-boot first, and to power on whenever it gets mains power.</p>

<p>The enrolment part is the lowest fidelity. Once the node is provisioned, burnt-in and enrolled as a compute instance in OpenStack I have two workloads it can run - either Nginx or Apache VMs. Those are lightweight and fast to start, and they prove connectivity through the network segments.</p>

<p alt="Fleet Manager machine list titled 'Machines discovered from MAAS and onboarded on Temporal', with two rows: 'large-foal' at 10.50.0.251 running the apache workload and 'key-newt' at 10.50.0.252 running nginx, both in the provisioned state and Deployed in MAAS, each linking to its latest Temporal workflow"><img src="/content/images/2026/06/fleet-manager-machines.png" alt="The Fleet Manager machine list, showing two NUCs discovered from MAAS and onboarded on Temporal" /></p>

<h2 id="the-lifecycle-in-temporal-workflows">The lifecycle in Temporal workflows</h2>

<p>Temporal is a powerful project for workflow management. It has built-in primitives for handling long-running tasks and managing failures. Workflows are defined in code using the Temporal SDK, where you define the orchestration steps. The real work happens in activities - also defined in code - which you execute from the workflow:</p>

<ul>
  <li>
    <p><strong>workflows</strong> need to be <em>deterministic</em> - producing the same output from the same input, with no side-effects. Temporal works by recording all activity in its event history. A workflow can be running but not active, if it’s waiting on an external event. When it activates again, it is loaded into a worker which runs a replay: executing the whole workflow again, but serving completed activities from the event history, before continuing with the next activity. If there are any inconsistencies from the replay Temporal throws <code class="language-plaintext highlighter-rouge">NondeterminismError</code>.</p>
  </li>
  <li>
    <p><strong>activities</strong> need to be <em>idempotent</em> - producing the same outcome if they are run repeatedly. Activities are automatically retried on failure by default (you can configure that in the workflow with <code class="language-plaintext highlighter-rouge">RetryPolicy</code>), so they need to be repeatable.</p>
  </li>
</ul>

<p>This is the simple <a href="https://github.com/sixeyed/fleet-lab/blob/main/packages/core/fm/workflows.py#L100">workflow for commissioning</a> a new machine in MAAS. This instructs MAAS to inventory the hardware and enrol the machine as ready for OS deployment:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">workflow</span><span class="p">.</span><span class="n">defn</span>
<span class="k">class</span> <span class="nc">CommissionMachine</span><span class="p">:</span>
    <span class="o">@</span><span class="n">workflow</span><span class="p">.</span><span class="n">run</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">system_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">_mark</span><span class="p">(</span><span class="n">system_id</span><span class="p">,</span> <span class="s">"provisioning"</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">workflow</span><span class="p">.</span><span class="n">execute_activity</span><span class="p">(</span><span class="s">"commission"</span><span class="p">,</span> <span class="n">system_id</span><span class="p">,</span> <span class="n">start_to_close_timeout</span><span class="o">=</span><span class="n">_ACT</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">_await_status</span><span class="p">(</span><span class="n">system_id</span><span class="p">,</span> <span class="s">"Ready"</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">_mark</span><span class="p">(</span><span class="n">system_id</span><span class="p">,</span> <span class="s">"new"</span><span class="p">)</span>  <span class="c1"># Ready in MAAS; not yet FM-provisioned
</span>        <span class="k">return</span> <span class="s">"ready"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">_mark</code> function is a helper that records the state of the workflow; <code class="language-plaintext highlighter-rouge">_await_status</code> checks the machine status with a poll-and-sleep loop. The real work happens in the <a href="https://github.com/sixeyed/fleet-lab/blob/main/packages/worker/fm_worker/activities.py#L66">commission activity</a> which calls into the MAAS API to commission the machine:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">activity</span><span class="p">.</span><span class="n">defn</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">commission</span><span class="p">(</span><span class="n">system_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">force</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">m</span> <span class="o">=</span> <span class="k">await</span> <span class="n">_ctx</span><span class="p">.</span><span class="n">maas</span><span class="p">.</span><span class="n">machine</span><span class="p">(</span><span class="n">system_id</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">m</span><span class="p">.</span><span class="n">status</span> <span class="ow">in</span> <span class="p">(</span><span class="s">"Commissioning"</span><span class="p">,</span> <span class="s">"Testing"</span><span class="p">):</span>
        <span class="k">return</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">force</span> <span class="ow">and</span> <span class="n">m</span><span class="p">.</span><span class="n">status</span> <span class="ow">in</span> <span class="p">(</span><span class="s">"Ready"</span><span class="p">,</span> <span class="s">"Deployed"</span><span class="p">):</span>
        <span class="k">return</span>
    <span class="k">await</span> <span class="n">_ctx</span><span class="p">.</span><span class="n">maas</span><span class="p">.</span><span class="n">commission</span><span class="p">(</span><span class="n">system_id</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">CommissionMachine</code> workflow gets called as a child workflow of <code class="language-plaintext highlighter-rouge">ProvisionMachine</code>, which co-ordinates the full machine onboarding:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ProvisionMachine
 ├── CommissionMachine   # MAAS - enrol machine (skip if done)
 ├── DeployMachine       # MAAS - install OS, configure network
 ├── BurnInMachine       # Slurm - run stress-ng, logs posted to Fleet Manager
 ├── RegisterCompute     # OpenStack - add the node as a nova-compute host
 └── AllocateWorkload    # OpenStack - launch a workload VM
</code></pre></div></div>

<p>All workflows in Temporal are durable - the event history stores everything (backed by Postgres in my lab, swappable for Cassandra at ultra-scale). That powers reliability because failures can be recovered at pretty much any level, providing the state store survives. It also supports long-running workflows - you can call <code class="language-plaintext highlighter-rouge">workflow.sleep</code> for a week if you need to. In my lab, deploying the OS can take 5 minutes, burn-in another 5, registration with OpenStack nearly 10. The full provisioning of a new machine takes about 25 minutes. The event history also powers observability:</p>

<p alt="Temporal Web UI showing the completed onboard workflow 'onboard-fwbccc' (ProvisionMachine), with a timeline of its child workflows - deploy_status, CommissionMachine, DeployMachine, BurnInMachine, RegisterCompute and AllocateWorkload - laid out as bars spanning roughly 25 minutes"><img src="/content/images/2026/06/temporal-provisioning-workflow.png" alt="The Temporal Web UI timeline for a ProvisionMachine workflow, with each child workflow shown as a bar across about 25 minutes" /></p>

<p>The end state is fully automated: plug in a NUC which is set for PXE boot and start on power cycle, and it gets managed from enrolment to running a workload, with no manual intervention.</p>

<h3 id="scaling-temporal-workers">Scaling Temporal workers</h3>

<p>Activities are custom code, run by workers which register in Temporal <em>task queues</em>. My <a href="https://github.com/sixeyed/fleet-lab/blob/main/docker/worker/Dockerfile">worker</a> is a single Docker image which has the logic to run any activity, but the workload is split by queue to support independent scaling. The Fleet Manager runs on Kubernetes, with <a href="https://keda.sh">KEDA</a> to scale workers with the <a href="https://keda.sh/docs/2.21/scalers/temporal/">Temporal scaler</a>, with different configurations per queue:</p>

<table>
  <thead>
    <tr>
      <th>Queue</th>
      <th>Logic</th>
      <th>Scale</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scan</code></td>
      <td>schedule to check for new machines</td>
      <td>scale-to-zero, pulses 0→1→0 each tick</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lifecycle</code></td>
      <td>machine workflows + short MAAS calls</td>
      <td>scale-to-zero between jobs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ops</code></td>
      <td>long backend activities (burn-in, register, deploy workload)</td>
      <td>always-on, fixed at 1</td>
    </tr>
  </tbody>
</table>

<p>Keeping the <code class="language-plaintext highlighter-rouge">ops</code> queue always-on lets the <code class="language-plaintext highlighter-rouge">lifecycle</code> worker(s) scale to zero. A lifecycle workflow hands off the slow work - like the ~5m burn-in - to the <code class="language-plaintext highlighter-rouge">ops</code> queue, then waits on a Temporal timer rather than staying active in a worker. During the long waits there’s nothing sitting on the <code class="language-plaintext highlighter-rouge">lifecycle</code> queue, and KEDA scales to zero. If there’s lots of work queued in Temporal, KEDA will scale up more worker Pods - any Pod can pick up any workflow, replay it and continue execution.</p>

<h2 id="the-learning-points">The learning points</h2>

<p>I ran my Fleet Manager over a couple of weeks, iterating first on a single NUC to get the provisioning workflow fully reliable. Then plugging in the second NUC to prove the full hands-off provisioning. There were a few interesting problems, some were down to the lab hardware, some took experimentation on the workflow stages, others were implementation issues.</p>

<p><strong>Activity idempotency.</strong> FM auto-commissions a node with MAAS as soon as it enrols. The <code class="language-plaintext highlighter-rouge">commission</code> activity needs to skip if the node is already commissioning or ready - otherwise a Temporal retry could land during a brief <code class="language-plaintext highlighter-rouge">Ready</code> window and re-commission the node, so you get into an endless commission loop. Same with deploy: the API itself isn’t idempotent, so the activity has to tolerate an HTTP 409 (“already deploying”) or a retry-after-success blocks the workflow. In durable orchestration at scale you can assume all the activity logic will be retried eventually, so you need to code for any initial state. You’ll only uncover edge cases with lots of testing.</p>

<p><strong>Exiting cleanly.</strong> Another edge case from testing - pulling a provisioned node out of OpenStack so it can be recommissioned (for hardware changes or drift correction). I deploy OpenStack with <a href="https://canonical.com/openstack">Canonical’s Sunbeam</a>, which keeps its control-plane state in a <a href="https://dqlite.io">dqlite</a> cluster - the Sunbeam microcluster. I run a single control node, but each fleet NUC also joins the microcluster, and dqlite makes it a voting member to maintain quorum. A failed or partial removal of a NUC leaves a stale voter behind, which breaks the quorum, and then the whole Sunbeam control plane stops responding. You can’t tell dqlite that certain classes of node should never be voters, so the fix is to ensure clean removal before MAAS wipes the disk and redeploys. That’s actually three steps: tear down the Juju machine first, then remove the node from the Sunbeam microcluster, then delete the stale compute record from Nova. That took experimentation too - when I’d thought <em>reclaim</em> would be the easiest part of the lifecycle.</p>

<p><strong>Token expiration.</strong> This was a good one. I left the system alone for a couple of days with a fully provisioned NUC, then came back and tried to reallocate the workload. This is a UI-triggered workflow which uses the OpenStack API to remove the running instance (which was Nginx) and deploy a replacement (Apache). I had tested all this before, but this time the workflow sat without progress for a long time, then the activity failed, then retried, then went into the same loop. Temporal records activity events but it doesn’t surface the logs - for that you need to dig into the worker logs. I found the Juju credentials had expired and the activity was waiting on a password prompt that was never going to be answered. The fix was to store the credentials in Juju’s <code class="language-plaintext highlighter-rouge">accounts.yaml</code> on the OpenStack node, so it stayed authenticated and didn’t prompt the activity for auth. The classic sort of day-2 ops issue you only find with longitudinal testing.</p>

<p><strong>More NICs please.</strong> My NUCs only have one onboard NIC, which is wired to the provisioning network and owned by MAAS. When OpenStack allocates a workload VM it sets it up with a real IP address on the same VLAN - but there’s no way to route to that, because the only NIC is already taken with the MAAS DHCP address. I experimented for a while with trying to set up a bridge but that was a dead end: both OpenStack and MAAS need to own the network config. This was a hardware fix - adding the USB NIC to get a separate network route which OpenStack could own as the provider uplink, with no host IP configured. Even then the config is fiddly: the <code class="language-plaintext highlighter-rouge">openstack-hypervisor</code> snap needs to set up the NIC through its own config otherwise it deletes changes from other processes.</p>

<p><strong>The not-so-smart plug.</strong> My plugs are simple Tapo P100 units which don’t have power sensing, so we can tell if the plug is on (via <a href="https://www.home-assistant.io">Home Assistant</a>) but not if the NUC itself is on. MAAS does soft resets during commissioning and deployment - effectively running <code class="language-plaintext highlighter-rouge">shutdown</code> -  which leaves the plug ON but the machine OFF. Ask the plug to turn on and it reports “already on” and does nothing, while the NUC is still off. The fix is a hack in the power-shim API - the <code class="language-plaintext highlighter-rouge">on</code> endpoint does an AC cycle - off, pause, on. That ensures the box is running when the plug is powered on (at the cost of resetting if it is already on). The deploy and commission workflows check if we’ve had no activity from MAAS for a suspicious period and then do the power cycle to ensure the box is running again. Sounds nasty but works reliably.</p>

<h2 id="testing-with-a-new-machine">Testing with a new machine</h2>

<p>After ironing out all of the edge cases on <code class="language-plaintext highlighter-rouge">large-foal</code> (MAAS chooses the name), I plugged in a second NUC with the BIOS configured, and the MAC address already set in the power-shim API. It took a minute or so for the new unit to show in Fleet Manager - but the scheduled scan workflow ran, found the machine and started onboarding.</p>

<p>The full cycle ran fully automated and gave me an Nginx site running on <code class="language-plaintext highlighter-rouge">key-newt</code>, which I could reach at the OpenStack-allocated IP address on the VLAN, from a machine on my normal network:</p>

<p alt="Fleet Manager machine detail page for the node 'key-newt' at 10.50.0.252 with 4 CPU and 16 GB, MAAS status Deployed, showing the lifecycle bar advanced to Registered, the node running the nginx workload at 10.50.0.205, Reprovision and Recommission buttons, and a table of completed Temporal workflows from onboard through AllocateWorkload"><img src="/content/images/2026/06/fleet-nuc-02.png" alt="The Fleet Manager detail page for the second NUC, fully provisioned and running an Nginx workload" /></p>

<p>This screenshot is snipped after the workflows table (which links to all workflows for the machine). The full UI also shows tables for OpenStack, MAAS and Slurm showing the event history and logs of the provisioning process.</p>

<h2 id="next-steps">Next steps</h2>

<p>This was a nice exploration of Temporal. The integration side is built as adapters which are fairly standard - using the REST API for MAAS and the Python SDK for OpenStack. Coding the Temporal activities to make them idempotent and the workflows to make them deterministic was new for me, and it’s a really nice approach. Having run Temporal for a couple of weeks now I can see a couple of projects where it might be a nice fit.</p>

<p>The next step for the home lab would be to deploy <a href="https://github.com/prometheus/node_exporter">node-exporter</a> and register new nodes with Prometheus during provisioning, so when the onboarding is done the new machines show up in my usual Grafana dashboards. But really OpenStack is too heavy for my workloads. Good to experiment with it (my <a href="https://github.com/sixeyed/fleet-lab/tree/main/infrastructure/openstack/ansible">Ansible setup</a> deploys it as an LXD VM for easy setup and cleanup), but it really needs multiple control plane nodes and I prefer to run my own workloads in Kubernetes.</p>

<p>So I could swap out the final <code class="language-plaintext highlighter-rouge">RegisterCompute</code> workflow with an equivalent set of steps to join my Kubernetes cluster. But I think I’ll stop here for now :)</p>

<p>The whole project is on GitHub. That’s the Python code, Helm charts, Ansible playbooks and infrastructure setup (the k3d part is reusable - the <code class="language-plaintext highlighter-rouge">sixeyed</code> part is my own cluster, but left there for reference). And the docs show the evolution of the build with the phased approach and the design docs for each component:</p>

<blockquote>
  <p><a href="https://github.com/sixeyed/fleet-lab">github.com/sixeyed/fleet-lab</a></p>
</blockquote>

<p>You can check out the stubbed version with only <a href="https://www.docker.com">Docker</a> and <a href="https://k3d.io">k3d</a> (and Python) as dependencies:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># create k3d cluster, deploy Temporal etc.
python ./infrastructure/k3d/setup.py

# build local Docker images, import to k3d and deploy the app:
python ./infrastructure/k3d/deploy.py --build
</code></pre></div></div>

<p>Then browse to the app on http://localhost:8088/, open the <em>Simulator</em> page and add a new machine. You can track all the workflows in Temporal at http://localhost:8233.</p>

<h2 id="faq">FAQ</h2>

<h3 id="can-i-use-this-for-my-neocloud-startup-if-i-buy-500m-of-compute">Can I use this for my neocloud startup if I buy $500M of compute?</h3>

<p>My repo is MIT-licensed, so you’re welcome to try. The orchestration pattern is the same whether the node is a $50K GPU server or a 4-core Intel NUC - you would replace <code class="language-plaintext highlighter-rouge">stress-ng</code> in the burn-in activity with <code class="language-plaintext highlighter-rouge">gpu-burn</code> and your final compute enrolment. In theory MAAS, Temporal, Slurm and OpenStack would work but you might need to tweak some of my code…</p>

<h3 id="what-does-each-tool-in-the-stack-do">What does each tool in the stack do?</h3>

<p>MAAS is the bare-metal provisioner - it takes control of the machine when it does a network boot, then enrols, commissions and deploys an OS. Temporal runs the durable lifecycle workflows that drive everything else. Slurm runs the burn-in stress test. OpenStack turns a burned-in node into compute capacity and launches a workload VM on it. A custom Python app - the Fleet Manager - ties them together.</p>

<h3 id="why-use-smart-plugs-instead-of-redfish-and-bmc">Why use smart plugs instead of Redfish and BMC?</h3>

<p>Reality. A real server has a baseboard management controller (BMC) that speaks Redfish (or IPMI) for remote power and boot control. Consumer NUCs have none of that, so I use a Tapo smart plug as the power control and a one-time BIOS setting to make each NUC PXE-boot on power-on. The plug gives me power on/off/query, which is enough for the lifecycle - with that AC cycle hack.</p>

<h3 id="can-you-demo-the-lifecycle-without-any-hardware">Can you demo the lifecycle without any hardware?</h3>

<p>Yes - that’s the k3d part. It deploys the real Fleet Manager app and Temporal to a new k3d cluster, with stub adapters for MAAS, Slurm and OpenStack. You can test and prove the durable onboarding and reprovision loop and see the lifecycle without a fleet.</p>

<h3 id="why-orchestrate-with-temporal-instead-of-cron-jobs-or-scripts">Why orchestrate with Temporal instead of cron jobs or scripts?</h3>

<p>Provisioning a node is a long, failure-prone, multi-step process - deploy, burn-in, register, allocate - 25m if all goes well, but it could fail at any step. Temporal gives you durable execution: a workflow survives restarts, retries activities, and you can see every step in the web UI.</p>

<h3 id="can-you-change-a-nodes-workload-without-reprovisioning">Can you change a node’s workload without reprovisioning?</h3>

<p>Yes. The workload runs as a VM on the node and is managed independently of the provisioning lifecycle, so a Reallocate action swaps it in place - it removes the running instance (say Nginx) and deploys a replacement (Apache) without wiping or re-onboarding the node. Reprovisioning is the heavier path that wipes the disk and runs the whole lifecycle again.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="homelab" /><category term="bare-metal" /><category term="maas" /><category term="temporal" /><category term="openstack" /><category term="slurm" /><category term="kubernetes" /><category term="devops" /><summary type="html"><![CDATA[I rebuilt the node lifecycle from Nscale's Fleet Operations in a home lab - MAAS, Temporal, Slurm and OpenStack provisioning Intel NUCs from rack to workload to reclaim.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2026/06/fleet-nuc-lifecycle.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2026/06/fleet-nuc-lifecycle.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mac Studio: The Best Local LLM Workstation Money Can(‘t) Buy</title><link href="https://blog.sixeyed.com/mac-studio-llm-workstation/" rel="alternate" type="text/html" title="Mac Studio: The Best Local LLM Workstation Money Can(‘t) Buy" /><published>2026-06-22T09:00:00+00:00</published><updated>2026-06-22T09:00:00+00:00</updated><id>https://blog.sixeyed.com/mac-studio-llm-workstation</id><content type="html" xml:base="https://blog.sixeyed.com/mac-studio-llm-workstation/"><![CDATA[<p>Frontier AI models from Anthropic, OpenAI etc. are closed source: they run in the provider’s cloud compute and you can only use them from the provider’s own platform. Open-weight models are alternatives which you can run yourself on fairly modest hardware. They’re reckoned to be about 9 months behind the frontier models; my preference is <a href="https://github.com/QwenLM/Qwen3">Qwen</a> (from Alibaba), and it is a very capable model for AI-first coding - it feels like where Sonnet was ~6-9 months ago.</p>

<p>You don’t need super high-end kit to run a local model, and in the latest VS Code release you can use Copilot directly with your own LLM. No prompts or responses ever leave your network, you’re not charged for any tokens, no presidents can turn off your access, and your data won’t be used to train the next generation.</p>

<p>I started running smaller models on my old M1 Mac Studio last year and hit two problems: 64GB of memory is not enough to run a high-fidelity model, and the speed and number of GPU cores on my box meant responses were too slow for daily use. So I did a bunch of research and finally settled on what I think is the best box for local inference: a newer Mac Studio :)</p>

<p>The Mac Studio M4 Max with 128GB of unified memory is a very performant LLM workstation. It’s Apple Silicon, which is ARM64 architecture - and that runs efficiently. It’s quieter, cooler and cheaper to run than the alternatives, and far simpler to buy than to build something yourself.</p>

<p class="notice--info">Actually you can’t buy it today. I bought mine at the end of 2025, and I had the choice of M4 Max up to 128GB or M3 Ultra up to 512GB. Now the RAM shortage has even hit Apple and 96GB is the most you can buy (and prices are going up). It’s not clear if the M5 Max/Ultra Studios will launch this year, and how much RAM they’ll have if they do.</p>

<p>As I write you can get an M5 Max MacBook Pro with 128GB. If you can get hold of a suitable machine, you can use the scripts and config on my repo to get up and running quickly: <a href="https://github.com/sixeyed/local-llms">sixeyed/local-llms on GitHub</a>.</p>

<p class="notice--info"><strong>TL;DR:</strong> The M4 Max Mac Studio with 128GB of unified memory is the best local LLM workstation for developers right now. The unified memory lets it load models a dual-GPU PC physically can’t, its memory bandwidth makes inference faster than equivalent boxes, and it runs silently on a very small power draw. I run Qwen3, Ling and Gemma on it with llama.cpp and MLX, and <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> is fast and capable enough for daily coding.</p>

<h2 id="apples-unified-memory-architecture">Apple’s unified memory architecture</h2>

<p>LLMs are memory hogs. All model weights have to be loaded to memory before you can use them, and the good models are big. A 122-billion parameter model needs around 77GB just to load at a sensible quantization - and that’s before you add in the prompt’s context window, which uses far more memory server-side than you would expect.</p>

<p>Traditional domestic GPU setups don’t scale to these large models. Even a high-end NVIDIA card (like the RTX 4090) only has 24GB of VRAM. You could buy two and wire them together, but now you’re looking at £1,000s in graphics cards alone, plus the practical difficulties of rigging and managing multi-GPU configuration, and you’ve still only got 48GB to play with. You would have to run smaller models, or full-sized models at higher quants (which means lower fidelity and worse performance).</p>

<p>Apple Silicon has a <a href="https://www.apple.com/newsroom/2020/11/apple-unleashes-m1/">unified memory architecture</a>. The 128GB in my Mac Studio is one pool of memory, shared between the CPU and GPU cores. Metal (Apple’s GPU framework) can use almost all of the system memory. On my M4 Max, very capable coding models can take 100GB+ which still leaves plenty for the OS and everything else. That’s roughly four times what you get from a single high-end NVIDIA card.</p>

<p>The Mac Studio isn’t the only unified-memory box. The <a href="https://frame.work/desktop">Framework Desktop</a> uses the same idea with AMD’s Ryzen AI Max+ 395, and NVIDIA’s <a href="https://www.nvidia.com/en-us/products/workstations/dgx-spark/">DGX Spark</a> does it with the GB10 Grace Blackwell chip - both put 128GB in one pool shared between CPU and GPU. The Spark pairs an Arm CPU with an NVIDIA GPU; the Framework runs an x86 chip with an integrated Radeon GPU. Both are capable machines - but the memory bus on each is much slower than the Mac’s, and for inference that’s the number that counts.</p>

<p>Inference is memory-bound - generating each token means reading the whole active model out of memory. The speed that data moves from memory to the GPU cores is the limiter for how fast your model responds. Apple’s memory bandwidth is about twice the others’:</p>

<table>
  <thead>
    <tr>
      <th>Machine</th>
      <th>Unified memory (max)</th>
      <th>Memory bandwidth</th>
      <th>GPU cores (max)</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Mac Studio (M4 Max)</strong></td>
      <td>128GB LPDDR5x</td>
      <td><strong>546 GB/s</strong></td>
      <td>40 (Apple)</td>
      <td>~£3,800 (Nov ‘25)</td>
    </tr>
    <tr>
      <td><strong>MacBook Pro (M5 Max)</strong></td>
      <td>128GB LPDDR5x</td>
      <td><strong>614 GB/s</strong></td>
      <td>40 (Apple)</td>
      <td>~£5,100</td>
    </tr>
    <tr>
      <td><strong>NVIDIA DGX Spark (GB10)</strong></td>
      <td>128GB LPDDR5x</td>
      <td>273 GB/s</td>
      <td>6,144 (CUDA)</td>
      <td>~£4,600</td>
    </tr>
    <tr>
      <td><strong>Framework Desktop (Ryzen AI Max+ 395)</strong></td>
      <td>128GB LPDDR5x</td>
      <td>256 GB/s</td>
      <td>40 (RDNA 3.5 CU)</td>
      <td>~£4,200</td>
    </tr>
  </tbody>
</table>

<p class="notice--info">Don’t read the GPU-core counts as a league table - they’re different units. An Apple GPU core, an AMD compute unit, and an NVIDIA CUDA core aren’t the same thing, so the Spark’s 6,144 “cores” aren’t 150 times the Mac’s 40 - each Apple core packs roughly 128 ALUs of its own.</p>

<p>Those prices are June 2026. They’ve all gone up about £1000 since January. The M5 Max is a laptop, but it’s the only Apple Silicon you can still buy at 128GB - and at 614 GB/s it’s faster than my Studio. The M4 Max Studio price is what I paid in late 2025; maybe it will come back…</p>

<p>They all fit the same big models, but the memory bandwidth should give the Mac boxes the edge. I haven’t spent £10K on the other boxes to run a comparison - but if NVIDIA or Framework want to send me one, I’d be happy to write it up :) It’s reasonable to assume the Studio decodes about twice as fast as the Spark or the Framework would. In inference terms, that means higher tokens per second, and a better user experience.</p>

<h2 id="inference-engines-llamacpp-and-mlx-not-ollama">Inference engines: llama.cpp and MLX, not Ollama</h2>

<p>If you’ve dabbled with local models you’ve probably used Ollama. It’s a great on-ramp, super easy to use and lots of models available. But it doesn’t have all the tweaks to configure model performance, and there can be a lag between models being released on Hugging Face and making it through to Ollama.</p>

<p>There are more powerful inference engines which are Mac friendly:</p>

<ul>
  <li><strong><a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a></strong> - runs GGUF models with fine-grained control over context and quantization. A good fit for the Qwen3 family.</li>
  <li><strong><a href="https://github.com/ml-explore/mlx">MLX</a></strong> - Apple’s own framework, which runs MLX-format models natively on Apple Silicon. Use for models that are only published as MLX weights, like Ling 2.6 Flash and Gemma 4.</li>
</ul>

<p>Both serve an OpenAI-compatible API, so every client you might want to use talks to them the same way. I’ve tried Open WebUI, Cline, opencode and VS Code’s chat - they all work fine.</p>

<p>I also did a bunch of performance tuning with Claude. I used Claude Code to run the models and monitor the output logs while I ran a task in a client on a remote machine. Claude made performance recommendations, we tweaked the settings and ran again. Those optimizations are all in the GitHub repo, and the model-start process is in a Python script which has all the flags.</p>

<p>You can start from scratch with these commands. Model weights are pulled on first use, so that will take a few minutes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install the engines (macOS)</span>
brew <span class="nb">install </span>llama.cpp
pip3 <span class="nb">install</span> <span class="nt">--user</span> mlx-lm

<span class="c"># Launch a model - run.py picks the right engine for you</span>
python run.py                       <span class="c"># default: qwen3.6-moe via llama-server</span>

<span class="c"># or try other families:</span>
python run.py <span class="nt">--family</span> ling         <span class="c"># ling-2.6-flash via MLX</span>
python run.py <span class="nt">--family</span> gemma        <span class="c"># gemma-4-31b via MLX</span>

<span class="c"># Stop whatever's running</span>
python stop.py
</code></pre></div></div>

<p>I run this just for me, and the parameters are set for a good working context (250K), and a single prompt (no concurrent clients). Only one model runs at a time, so I use a standard port (8083) and then I can switch models without changing client config.</p>

<p>The optimizations in the repo are all tuned for the M4 Max (128GB): the right quantization for each model, context length pushed as far as the memory allows, and batch sizes set for this chip. The scripts detect whatever Mac you’re on and print its capabilities, but the numbers are tuned for this machine.</p>

<h2 id="usable-local-coding-models">Usable local coding models</h2>

<p>The model landscape moves fast, so treat this as a snapshot (I have a scheduled task in Claude which runs daily to research open-weight models and recommend any new ones). The big shift in the last year is mixture-of-experts (MoE) models, which have huge total parameter counts but only activate a subset of experts per request. So you get the knowledge of a big model with the speed of a small one. That combination is what makes local LLMs genuinely productive on (fairly) modest hardware.</p>

<p>Here’s what I’ve tried - all available in the scripts. I’ve put each model’s SWE-bench Verified score next to it - that’s the “can it fix a real GitHub issue on its own” benchmark. It’s an older benchmark and it’s not perfect but it’s a reasonable proxy for day-to-day coding usefulness:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Engine</th>
      <th>Params (active)</th>
      <th>SWE-bench Verified</th>
      <th>Best for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>qwen3.6-moe</td>
      <td>llama.cpp</td>
      <td>35B-A3B (MoE)</td>
      <td><a href="https://huggingface.co/Qwen/Qwen3.6-27B">73.4%</a></td>
      <td>Fast coding - my default</td>
    </tr>
    <tr>
      <td>qwen3.6</td>
      <td>llama.cpp</td>
      <td>27B (dense)</td>
      <td><a href="https://huggingface.co/Qwen/Qwen3.6-27B">77.2%</a></td>
      <td>General coding, agentic tasks</td>
    </tr>
    <tr>
      <td>qwen3.5</td>
      <td>llama.cpp</td>
      <td>122B-A10B (MoE)</td>
      <td><a href="https://huggingface.co/Qwen/Qwen3.5-122B-A10B">72.0%</a></td>
      <td>Large context, broad knowledge</td>
    </tr>
    <tr>
      <td>qwen3-coder-next</td>
      <td>llama.cpp</td>
      <td>80B-A3B (MoE)</td>
      <td><a href="https://arxiv.org/html/2603.00729v1">70.6%</a></td>
      <td>Code-specialized</td>
    </tr>
    <tr>
      <td>ling-2.6-flash</td>
      <td>MLX</td>
      <td>104B-A7.4B (MoE)</td>
      <td>not published</td>
      <td>Fast agentic tasks</td>
    </tr>
    <tr>
      <td>gemma-4-31b</td>
      <td>MLX</td>
      <td>31B (dense)</td>
      <td>not published</td>
      <td>Cline / agentic coding</td>
    </tr>
  </tbody>
</table>

<p>Those are the vendor- and community-reported SWE-bench Verified figures - rough ranking, because the hardware impacts the numbers. But it shows what <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> can do objectively: it lands at 73.4% with just 3 billion active parameters.</p>

<p>The frontier closed models still lead this benchmark - but the gap is quite narrow, except right at the top. <a href="https://www.anthropic.com/news/claude-sonnet-4-6">Claude Sonnet 4.6</a> scores 77.9% and <a href="https://openai.com/index/introducing-gpt-5/">GPT-5</a> 74.9% on SWE-bench Verified - only a few points above <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code>’s 73.4%. <a href="https://www.anthropic.com/news/claude-opus-4-8">Claude Opus 4.8</a> is the real coder at 88.6%, so the flagship still has the edge. But those models run in someone else’s datacenter on metered tokens, and this one runs silently on a box under my desk.</p>

<p>The model families worth trying mainly come from three providers:</p>

<ul>
  <li><strong><a href="https://github.com/QwenLM/Qwen3">Qwen</a></strong> (Alibaba Cloud) - the most capable open-weight coding models I’ve used. The <code class="language-plaintext highlighter-rouge">qwen3.x</code> releases come in both dense and MoE variants, and run on llama.cpp as GGUF.</li>
  <li><strong><a href="https://github.com/inclusionAI/Ling">Ling</a></strong> (Ant Group’s inclusionAI) - a MoE-first family tuned for fast agentic work. Ling 2.6 Flash ships as MLX weights, so it runs natively on Apple Silicon.</li>
  <li><strong><a href="https://deepmind.google/models/gemma">Gemma</a></strong> (Google DeepMind) - Google’s open-weight models, built from the same research as Gemini. Dense, easy to run, and solid for agentic coding in Cline.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> is genuinely usable for planning, debugging and coding. It’s big enough to handle difficult, long-running tasks, and it’s fast enough to feel responsive. It works nicely with my <a href="/monitor-ai-agents-with-rocket-chat/">centralized chat interface</a> and it supports tool use and vision.</p>

<h2 id="connecting-your-tools">Connecting your tools</h2>

<p>The OpenAI-compatible API makes local models available to most tools. I point clients at the Studio with the base URL as the host and port plus <code class="language-plaintext highlighter-rouge">/v1</code>, and the API key can be any non-empty string because the backend inference engines ignore it.</p>

<p>VS Code’s chat <a href="https://code.visualstudio.com/updates/v1_121#_custom-endpoint-provider-for-byok-insiders">supports custom OpenAI-compatible models</a>. Make sure you set your agents to run <em>Local</em> and in the model selector click the cog to add a new model:</p>

<p alt="VS Code chat model picker with the agent mode set to Local and the cog icon for adding a new custom model endpoint"><img src="/content/images/2026/06/vs-code-configure-agent.png" alt="Configuring a custom OpenAI-compatible model in VS Code" /></p>

<p>Then you drop into the JSON editor and you can set up your local model connection:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.101:8083/v1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vendor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customendpoint"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"apiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"apiType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chat-completions"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qwen3.6-moe"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qwen3.6-moe"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.101:8083/v1/chat/completions"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"toolCalling"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"vision"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"maxInputTokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">250000</span><span class="p">,</span><span class="w">
        </span><span class="nl">"maxOutputTokens"</span><span class="p">:</span><span class="w"> </span><span class="mi">16000</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">name</code> and <code class="language-plaintext highlighter-rouge">url</code> point at the Studio on my network; <code class="language-plaintext highlighter-rouge">toolCalling</code> and <code class="language-plaintext highlighter-rouge">vision</code> switch on the features <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> supports, and the token limits match the 250K context the server is tuned for. I also run Open WebUI on my network, and that also connects to Qwen.</p>

<h2 id="beware-thinking-mode">Beware thinking-mode</h2>

<p>The Qwen3 models can do <em>thinking</em> - generating a chain of reasoning before they answer. It’s usually a good option for one-off questions, but coding agents can get confused. Cline and VS Code’s agent can get tripped up with Qwen3: sometimes it gets stuck thinking. It writes “let me investigate…” and keeps deferring instead of committing to an action, and it’ll keep looping in the client and never make any progress.</p>

<p>My launcher disables thinking by default - it sets <code class="language-plaintext highlighter-rouge">--reasoning-budget 0</code>, which makes the model commit straight to output. I haven’t seen any drop in quality on multi-step planning, and the shoegazing think-loop has gone. If you want to experiment with thinking back on, the repo documents how to dial it back up with a capped budget. But for day-to-day agentic coding, off is going to be fine.</p>

<h2 id="the-bottom-line">The bottom line</h2>

<p>The Mac Studio isn’t really sold as an AI machine, but for running large models locally it’s hard to beat. The unified memory lets you load models that are genuinely capable, and the latest MoE models run fast enough for interactive work. The Studio is built to run efficiently and quietly. I have mine on 24x7 and never notice it; if it does make a noise it’s drowned out by the Windows laptops I have running which are doing nothing complicated at all.</p>

<p>A dual-GPU rig will out-pace the Mac on inference for models that fit in 48GB - it has the raw decode speed. But a custom rig will probably cost more to buy, draw ten times the power to run and sound like a jet engine. And it still can’t load the big models. For doing real work with local LLMs, a unified memory architecture is the enabler, and the Mac is the winner for me.</p>

<p>The golden age of cheap AI may be coming to an end, and then the development cycle will need AI assistance running on your own hardware. You’ll use the local model for all the straightforward tasks, and delegate to Claude when you have something really complex to do - maybe Claude Code will evolve to use <a href="/claude-is-coming-for-your-job/">Opus as the director</a>, and transfer work between Sonnet and local LLM subagents.</p>

<h2 id="faq">FAQ</h2>

<h3 id="can-you-run-really-run-local-coding-llms-on-a-mac">Can you run really run local coding LLMs on a Mac?</h3>

<p>Yes - Apple Silicon Macs are great machines for local inference, thanks to their unified memory architecture. The CPU and GPU share one large memory pool, so the GPU can address much more memory than a separate GPU card. Install llama.cpp (<code class="language-plaintext highlighter-rouge">brew install llama.cpp</code>) or MLX (<code class="language-plaintext highlighter-rouge">pip3 install mlx-lm</code>), pull a model, and point any OpenAI-compatible client at it.</p>

<h3 id="how-much-memory-do-you-need-to-run-a-local-llm">How much memory do you need to run a local LLM?</h3>

<p>It depends on the model. A 7B model runs in around 16GB; mid-size MoE coding models like <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> are comfortable in 64GB; and the largest model I run (a 122B MoE) needs 100GB+ once you include the context window. For serious coding work, 128GB of unified memory leaves you some headroom. The Macs topping out at 96GB would probably need a smaller context window than my 250K.</p>

<h3 id="is-the-mac-studio-better-than-an-nvidia-gpu-for-ai">Is the Mac Studio better than an NVIDIA GPU for AI?</h3>

<p>I haven’t done a direct comparison, but the numbers suggest it is. A single RTX 4090 has only 24GB of VRAM, so it can’t load a 77GB model at all, while a 128GB Mac Studio can. A dual-GPU rig will decode faster for models that fit in its VRAM, but it costs more, draws around ten times the power, and still can’t load the big models. For raw speed on small models the GPU wins; for capacity, efficiency and quiet, the Mac wins.</p>

<h3 id="which-local-model-is-best-for-coding">Which local model is best for coding?</h3>

<p>I’ve tried a few and I’ve settled on <code class="language-plaintext highlighter-rouge">qwen3.6-moe</code> - it scores 73.4% on SWE-bench Verified with just 3 billion active parameters, so it’s fast and genuinely useful for planning, debugging and agentic coding. The Qwen3 family overall is the most capable open-weight option I’ve used.</p>

<h3 id="should-i-use-ollama-for-local-models">Should I use Ollama for local models?</h3>

<p>Ollama is the easiest on-ramp, but for performance tuning I prefer llama.cpp (for GGUF models) and MLX (for Apple-native MLX weights). They give finer control over context length and quantization, and you get new models sooner than waiting for them to land in Ollama.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="ai" /><category term="llm" /><category term="local-llm" /><category term="apple-silicon" /><category term="mac-studio" /><category term="llama-cpp" /><category term="mlx" /><category term="developer-productivity" /><summary type="html"><![CDATA[Why an M4 Max Mac Studio with 128GB of unified memory is the best local LLM workstation for developers - running Qwen3, Ling and Gemma with llama.cpp and MLX.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2026/06/openwebui-qwen.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2026/06/openwebui-qwen.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">All Agents Report Back to Me: Monitoring AI Agents with Chat</title><link href="https://blog.sixeyed.com/monitor-ai-agents-with-rocket-chat/" rel="alternate" type="text/html" title="All Agents Report Back to Me: Monitoring AI Agents with Chat" /><published>2026-06-15T09:00:00+00:00</published><updated>2026-06-15T09:00:00+00:00</updated><id>https://blog.sixeyed.com/monitor-ai-agents-with-rocket-chat</id><content type="html" xml:base="https://blog.sixeyed.com/monitor-ai-agents-with-rocket-chat/"><![CDATA[<p>I use agents all day, every day. The tools and models keep getting better, but the management experience is still cumbersome - especially if you use different tools and different models on different machines. I’m regularly <a href="/ten-tips-claude-code/">running multiple Claude Code sessions</a> on different boxes, plus Windsurf (with Claude) and Cline (with Qwen running on my Mac Studio). We get <a href="/claude-is-coming-for-your-job/">a powerful lot of work done</a>, but I need to keep swapping between machines to check on progress and steer the agents.</p>

<p class="notice--info">Claude Code, Windsurf and Cline each have an agent view where you can monitor activity across agents. But that’s only the agents in <em>that tool</em> on <em>that machine</em>. I wanted a universal agent status portal. Which sounds a lot like a chat room.</p>

<p>The chat room idea works really nicely. You can kick off a long session - implementing a feature, monitoring a deployment, running a performance test - and tell the agent to use a skill to post updates to a discussion on the chat server. Each agent gets their own ID and each task gets its own discussion, so you can watch everything go from a central machine. Or when you leave the office for the evening, you can check in all those remote agents on your phone.</p>

<p>This post walks through the setup: a lightweight <a href="https://rocket.chat">Rocket.Chat</a> server running on my internal network that every agent posts to, plus a skill that wraps it so any agent can create a discussion, post updates, and take instructions back. The whole thing is in a reference repo on GitHub:</p>

<p class="notice--info"><a href="https://github.com/sixeyed/agent-chat">sixeyed/agent-chat</a></p>

<h2 id="quickstart">Quickstart</h2>

<p>There’s a <code class="language-plaintext highlighter-rouge">demo</code> folder in the repo that stands up the whole stack - ingress, cert-manager, MongoDB and Rocket.Chat - on a local <a href="https://k3d.io">k3d</a> cluster. You’ll need this installed to try it:</p>

<ul>
  <li><a href="https://www.docker.com/products/docker-desktop/">Docker Desktop</a> (or any Docker engine)</li>
  <li><a href="https://k3d.io/#installation">k3d</a></li>
  <li><a href="https://kubernetes.io/docs/tasks/tools/">kubectl</a></li>
  <li><a href="https://helm.sh/docs/intro/install/">helm</a></li>
</ul>

<p>It deploys the exact same Rocket.Chat chart as production, with a local-friendly values overlay:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/sixeyed/agent-chat
<span class="nb">cd </span>agent-chat/demo
./up.sh          <span class="c"># or ./up.ps1 if you prefer PowerShell</span>
</code></pre></div></div>

<p>The first run takes a few minutes while it pulls the images and runs Rocket.Chat’s first-boot migration. When it’s done it prints your connection details - the web UI at <code class="language-plaintext highlighter-rouge">https://rocketchat.localtest.me</code> (log in as <code class="language-plaintext highlighter-rouge">admin</code> / <code class="language-plaintext highlighter-rouge">demo1234</code>), the agent endpoint at <code class="language-plaintext highlighter-rouge">http://localhost:8099</code>, and the <code class="language-plaintext highlighter-rouge">RC_*</code> environment variables you paste into your agent’s shell to wire up the <code class="language-plaintext highlighter-rouge">post-chat</code> skill.</p>

<p>Ask an agent to post its progress, watch the discussion show up under <code class="language-plaintext highlighter-rouge">#agent</code> with a live unread badge, and when you’re done run <code class="language-plaintext highlighter-rouge">./down.sh</code> to delete the cluster and all the demo data. The rest of this post explains what’s actually going on under that one command.</p>

<h2 id="why-a-self-hosted-chat-server">Why a self-hosted chat server</h2>

<p>The chat server is a good fit. It’s the interface we humans already use to coordinate work. You’ve got multiple users, multiple threads, unread badges, web and mobile apps that push notifications. But agents post status that could leak all sorts of interesting details - so you don’t want that flowing through a third-party setup like Slack or Teams.</p>

<p>Rocket.Chat gives you all the features out of the box, and it’s open source so you can run it on your network. That keeps the data on the LAN and it means you own auth. Each agent has its own user account with clearly defined permissions, and they use access tokens to post to the REST API. If anything goes awry - say an agent posts a load of system credentials without thinking - that information is still private. If the agent’s chat account somehow gets compromised, the attack radius is super small and you can rotate the token or delete the account.</p>

<p>So the model is: a chat server lives on my internal network, agents post to it over the network, and I read it from a browser or my phone. I have dnsmasq and cert-manager running on my home Kubernetes cluster, with a custom certificate authority to run internal domains over HTTPS. On my laptop and phone I have the CA in the approved list, but for the agents running on different machines, I wanted to have a simple HTTP endpoint over an IP address.</p>

<h2 id="two-front-doors-https-for-you-http-for-the-agents">Two front doors: HTTPS for you, HTTP for the agents</h2>

<p>The Rocket.Chat web UI needs a browser “secure context” - things like <code class="language-plaintext highlighter-rouge">crypto.randomUUID</code> and notifications are only supported when the page is served over HTTPS. If you try to load the UI over plain HTTP on a LAN IP, you get empty channels and a broken experience. TLS is mandatory for the full UI experience in the browser and in the mobile apps.</p>

<p>Agents don’t use the browser - they call the REST API which doesn’t need a secure context. So agents can talk plain HTTP and they just need the IP address of the server, without needing all the DNS and CA setup that the full client needs.</p>

<p>The deployment is built with two front doors:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Humans (web, iOS)</th>
      <th>AI agents</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Connect over</td>
      <td><strong>HTTPS</strong> through an nginx ingress</td>
      <td>plain <strong>HTTP</strong> to a NodePort</td>
    </tr>
    <tr>
      <td>Endpoint</td>
      <td><code class="language-plaintext highlighter-rouge">https://rocketchat.mydomain</code></td>
      <td><code class="language-plaintext highlighter-rouge">http://192.168.2.xyz:8099</code></td>
    </tr>
    <tr>
      <td>Needs</td>
      <td>a CA-signed certificate and DNS (browser secure context)</td>
      <td>nothing but the server’s IP - no TLS, no DNS</td>
    </tr>
  </tbody>
</table>

<p>It’s the same Rocket.Chat instance behind both doors. The split is just about access paths: one needs a trusted certificate for the clients, the other doesn’t because it’s just for the API. They are not interchangeable - if you try to browse to the IP endpoint you’ll see the UI is broken. If you point an agent at the HTTPS endpoint it will need to have the CA cert approved and be able to reach the domain.</p>

<p alt="Architecture diagram showing humans connecting over HTTPS through an nginx ingress with a CA-signed certificate, and AI agents connecting over plain HTTP to a NodePort, both reaching the same Rocket.Chat instance backed by MongoDB on Kubernetes"><a href="https://github.com/sixeyed/agent-chat/blob/main/docs/agent-chat-architecture.png"><img src="/content/images/2026/06/agent-chat-architecture.png" alt="Architecture diagram showing humans connecting over HTTPS through an nginx ingress with a CA-signed certificate, and AI agents connecting over plain HTTP to a NodePort, both reaching the same Rocket.Chat instance backed by MongoDB on Kubernetes" /></a></p>

<p>The diagram above shows both paths into the cluster. Click it for the full-size version in the <a href="https://github.com/sixeyed/agent-chat/blob/main/docs/agent-chat-architecture.png">repo docs</a>.</p>

<p class="notice--info">This gives you secure access for human clients within the network. For external access I use Tailscale - my servers are all registered and the Kubernetes operator is a good fit.</p>

<h2 id="one-discussion-per-session">One discussion per session</h2>

<p>I have this set up so all agents post to one <em>channel</em> in Rocket.Chat, but with separate <em>discussions</em> for each session. That means you can subscribe to multiple discussions and see them all in your navigation menu, or flip back to the channel to see all the work in one view.</p>

<p>The skill tells agents to generate a discussion name unless the user provides one, in which case they should check for existing sessions instead of creating a new one.</p>

<p class="notice--info">Rocket.Chat does let you have multiple discussions with identical names. Sometimes agents get confused and create a new discussion with the same name, but you can work around that by being stricter with your prompts.</p>

<h2 id="the-post-chat-skill">The post-chat skill</h2>

<p>The logic for working with the REST API is completely generic, and one skill works across all models and tools:</p>

<ul>
  <li><a href="https://github.com/sixeyed/agent-chat/blob/main/skills/post-chat/SKILL.md"><code class="language-plaintext highlighter-rouge">SKILL.md</code></a> - instructions for the agent: find-or-create a discussion, post updates, and run the control channel.</li>
  <li><a href="https://github.com/sixeyed/agent-chat/blob/main/skills/post-chat/rc.sh"><code class="language-plaintext highlighter-rouge">rc.sh</code></a> / <a href="https://github.com/sixeyed/agent-chat/blob/main/skills/post-chat/rc.ps1"><code class="language-plaintext highlighter-rouge">rc.ps1</code></a> - interchangeable helper scripts that wrap the REST API calls and keep the auth token off the command line.</li>
</ul>

<p>The skill explicitly tells the agent to call the helper script, so none of that logic bulks up the context. It also tells the agent to look for a set of environment variables (prefixed with <code class="language-plaintext highlighter-rouge">RC_</code> in the demo) for the connection and auth details. You need to set these up manually on each machine, using the credentials you set up for that machine.</p>

<p>The helper script is simple for agents to use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">RID</span><span class="o">=</span><span class="si">$(</span><span class="s2">"</span><span class="nv">$RC</span><span class="s2">"</span> discussion <span class="s2">"Refactor auth module"</span><span class="si">)</span>   <span class="c"># find-or-create, returns the room id</span>
<span class="s2">"</span><span class="nv">$RC</span><span class="s2">"</span> post <span class="s2">"</span><span class="nv">$RID</span><span class="s2">"</span> <span class="s2">":hourglass: Running tests…"</span>
<span class="s2">"</span><span class="nv">$RC</span><span class="s2">"</span> post <span class="s2">"</span><span class="nv">$RID</span><span class="s2">"</span> <span class="s2">":white_check_mark: 42/42 passing. Done."</span>
<span class="s2">"</span><span class="nv">$RC</span><span class="s2">"</span> close <span class="s2">"</span><span class="nv">$RID</span><span class="s2">"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">discussion</code> subcommand is the find-or-create - call it with a stable name and you can resume the same room between sessions. <code class="language-plaintext highlighter-rouge">post</code> sends a message (markdown and <code class="language-plaintext highlighter-rouge">:emoji:</code> shortcodes both work, which is how you get the nice status icons). <code class="language-plaintext highlighter-rouge">close</code> tidies the discussion out of the sidebar when the work is done, but leaves it in place so the same name resumes it next time.</p>

<p>The skill is strict about the auth: <strong>the PAT is a secret, and it should never touch a command line or log.</strong> Agents authenticate with the Personal Access Token in the environment variable, and the helper passes it to the HTTP call in a way that keeps it out of process listings and logs - via a curl config file descriptor in bash, or in-process with <code class="language-plaintext highlighter-rouge">Invoke-RestMethod</code> in PowerShell.</p>

<p>The connection details come from the environment variables (<code class="language-plaintext highlighter-rouge">RC_URL</code>, <code class="language-plaintext highlighter-rouge">RC_PAT</code>, <code class="language-plaintext highlighter-rouge">RC_UID</code> etc.), which you can set per-session or machine-wide, depending on how brave you are. The PAT is a full credential - anyone holding one can post and act as that user - the skill protects it, but it’s up to you how you secure it.</p>

<blockquote>
  <p>The skill is cross-platform and environment variables are the lowest common denominator. If you’re only using one OS then you could change it to read from the macOS keychain, or whatever is more secure for your setup.</p>
</blockquote>

<h2 id="replying-to-steer-the-two-way-control-channel">Replying to steer: the two-way control channel</h2>

<p>Posting updates is actually the easy part, and all the tools and models I’ve tried can do that. The more interesting part is using the Rocket.Chat discussion as a control room - so agents post their updates, and look for responses (from a specific user) to steer the session:</p>

<p alt="A Rocket.Chat discussion where an AI agent has posted a progress update and is waiting for a reply from the controlling user to decide its next step"><img src="/content/images/2026/06/agent-wait-1.png" alt="A Rocket.Chat discussion where an AI agent has posted a progress update and is waiting for a reply from the controlling user to decide its next step" /></p>

<p>This is secured by user id. The skill only acts on messages from a specific controller account - which is your account over HTTPS - a random message in the room gets ignored. The trust boundary is the chat room’s access control plus that controller filter, because replies are executed verbatim with no extra verification.</p>

<blockquote>
  <p>You could extend this so the skill limited the actions it would take from the control room. But the current mode is YOLO - which is why this belongs on a trusted, internal server. You could make the allowlist multi-user, which turns this into a team control room for remote agents running on any machine.</p>
</blockquote>

<p>The mechanics of the loop are pretty efficient - although not all tools have a great sleep-and-wake process. A persistent <code class="language-plaintext highlighter-rouge">watch</code> runs in the background, polling Rocket.Chat for new controller messages. The polling itself costs no tokens - it’s just a shell loop - and the <em>only</em> thing that wakes the agent is an actual new message from you.</p>

<p>A session can sit idle for hours waiting on your input without burning through anything, then spring back to work as soon(ish) as you reply. It checkpoints on message timestamps so it never processes the same reply twice, and it ends cleanly when you send a stop word (<code class="language-plaintext highlighter-rouge">/stop</code> by default, which also works on the end of a message - “deploy it then /stop” does the deploy, then stops):</p>

<p alt="An AI agent picking up a reply from the controller in the Rocket.Chat discussion and acting on it, including ending the session cleanly when it receives the stop word"><img src="/content/images/2026/06/agent-wait-2.png" alt="An AI agent picking up a reply from the controller in the Rocket.Chat discussion and acting on it, including ending the session cleanly when it receives the stop word" /></p>

<p>Claude Code is smart enough to drive the control channel properly - it’ll poll the discussion and treat your replies as the next turn, for as long as the session runs. Other platforms are less reliable at long-term monitoring, so the posting side works everywhere but the two-way control side is hit and miss. If you’re on Claude Code, you get the full experience.</p>

<h2 id="deploying-the-server">Deploying the server</h2>

<p>The server side is a Helm chart that deploys Rocket.Chat plus MongoDB to Kubernetes. If you’ve got a cluster on your LAN, getting it running is a handful of commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># namespace + storage, then resolve dependencies and deploy:</span>
kubectl create namespace rocketchat
kubectl apply <span class="nt">-f</span> pvc.yaml <span class="nt">-n</span> rocketchat
helm dependency build <span class="nb">.</span>
helm upgrade <span class="nt">--install</span> rocket-chat <span class="nb">.</span> <span class="nt">-n</span> rocketchat

<span class="c"># wait for MongoDB and the replica-set init, then Rocket.Chat itself:</span>
kubectl <span class="nt">-n</span> rocketchat rollout status deploy/mongodb
kubectl <span class="nt">-n</span> rocketchat rollout status deploy/rocket-chat-rocketchat
</code></pre></div></div>

<p>The chart is a wrapper over the official <code class="language-plaintext highlighter-rouge">rocketchat</code> chart, with one important change: the official chart bundles Bitnami’s MongoDB, which you need to avoid because of <a href="https://github.com/bitnami/containers/issues/83267">Bitnami-geddon</a>. This chart ships its own minimal single-node MongoDB instead (<code class="language-plaintext highlighter-rouge">mongo:8.0</code>, running as a one-node replica set). It runs as a monolith - microservices and NATS turned off - which is plenty for this job and much less to operate.</p>

<p>The first boot takes a few minutes while it pulls the image and runs migrations, so don’t panic when the Rocket.Chat pod isn’t ready immediately. Re-running <code class="language-plaintext highlighter-rouge">helm upgrade</code> is safe - the MongoDB init job is idempotent and skips itself if the replica set already exists. Full deploy, verify, and configuration steps are in the chart’s README in the repo.</p>

<h2 id="wiring-up-an-agent">Wiring up an agent</h2>

<p>Once the server’s up, connecting an agent is three steps:</p>

<ol>
  <li><strong>Generate the credentials</strong> - create a user and PAT for each agent that will post messages.</li>
  <li><strong>Install the <code class="language-plaintext highlighter-rouge">post-chat</code> skill</strong> and set the <code class="language-plaintext highlighter-rouge">RC_*</code> environment variables for the machine - the base URL, the parent channel id, and that machine’s own user id and token.</li>
  <li><strong>Tell the agent to report back.</strong> In a session, just ask it to use the <code class="language-plaintext highlighter-rouge">post-chat</code> skill to post progress. Claude Code will pick up the control-channel side on its own and start polling for your replies, or you can explicitly ask the tool to do that (or skip it).</li>
</ol>

<p>That’s it. From then on, every long-running job shows up in the sidebar as its own discussion, ticking over with status updates, and you can jump in to steer any of them from your phone (if the tooling supports it).</p>

<h2 id="faq">FAQ</h2>

<p><strong>Do I need Kubernetes to run this?</strong>
The reference deployment is a Helm chart, and the demo runs on a local k3d cluster, so you need Kubernetes for this exact setup. But there’s nothing special about the architecture - it’s just Rocket.Chat and MongoDB, so any Rocket.Chat install works just as well. The agent side only talks to the REST API, so it doesn’t care how the server is hosted.</p>

<p><strong>Can agents connect over HTTPS instead of plain HTTP?</strong>
Yes. The HTTPS endpoint serves the API too, so an agent can use it - but then that machine needs to trust your CA and resolve the domain. Plain HTTP over the NodePort skips all of that, which is why it’s the easier path for machines scattered across your network.</p>

<p><strong>Is the two-way control channel safe on a shared network?</strong>
Not really - treat it as internal-only. Replies from the controller account are executed verbatim, secured by nothing more than the controller’s user id and the room’s access control - there’s no command verification. That’s fine on a trusted LAN, but LANs aren’t actually that trustworthy and you definitely shouldn’t expose it to a wider network. If you want it tighter, you can extend the skill to limit the actions it will take (or remove the control channel feature altogether).</p>

<h2 id="where-this-takes-you">Where this takes you</h2>

<p>I’ve been waiting for the Universal Control Plane of All Agents to land, but until someone builds it this is a pretty good attempt. The agent interfaces in Claude Code and Windsurf are good. The <a href="https://cline.bot/blog/announcing-kanban">Kanban view in Cline</a> is a great approach. But they’re all tied to a single-platform single-machine environment.</p>

<p>This approach fixes that, but it’s not a perfect solution. The control channel feature depends on the smarts of the tooling, and the security model is “trust your internal network” (which you shouldn’t). But the building blocks are all standard - a chat server with a REST API and a skill - and you can adapt it all to whatever security level and extra features you want.</p>

<p>The reference deployment, the chart, and the skill are all in the repo:</p>

<blockquote>
  <p><a href="https://github.com/sixeyed/agent-chat">github.com/sixeyed/agent-chat</a></p>
</blockquote>

<p>Give it a try and have all your agents report back to a single place. It’s nice to know you can check in a four-hour performance test running on a remote machine and steer progress while you’re in the middle of a triathlon.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="ai" /><category term="claude" /><category term="claude-code" /><category term="ai-agents" /><category term="rocket-chat" /><category term="kubernetes" /><category term="developer-productivity" /><summary type="html"><![CDATA[Run a self-hosted Rocket Chat server so your AI coding agents post progress to one chat — watch long-running sessions from your phone and reply to steer them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2026/06/rocket-chat-demo.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2026/06/rocket-chat-demo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why Would You Write a Book About Docker in 2025?</title><link href="https://blog.sixeyed.com/why-would-you-write-a-book-about-docker-in-2025/" rel="alternate" type="text/html" title="Why Would You Write a Book About Docker in 2025?" /><published>2025-10-22T09:00:00+00:00</published><updated>2025-10-22T09:00:00+00:00</updated><id>https://blog.sixeyed.com/why-would-you-write-a-book-about-docker-in-2025</id><content type="html" xml:base="https://blog.sixeyed.com/why-would-you-write-a-book-about-docker-in-2025/"><![CDATA[<h1 id="why-would-you-write-a-book-about-docker-in-2025">Why Would You Write a Book About Docker in 2025?</h1>

<p>Docker is everywhere. It’s the most sensible way to package and run applications. Every cloud platform supports it, every CI/CD pipeline uses it, any laptop can run it, and pretty much every development team has adopted or is adopting it.</p>

<p class="notice--info">So why did I write a second edition of <a href="https://www.manning.com/books/learn-docker-in-a-month-of-lunches-second-edition">Learn Docker in a Month of Lunches</a>?</p>

<p>This is why: most engineers learn Docker on the job. You need to containerize an app, so you cobble together a Dockerfile from Stack Overflow. You need to run multiple containers, so you get Claude to write you a Docker Compose file. It works, you ship it, and you move on. But that doesn’t get you an understanding of how Docker works or what it can do.</p>

<h2 id="the-reality-of-learning-docker-in-production">The Reality of Learning Docker in Production</h2>

<p>I’ve trained hundreds of people on Docker and Kubernetes, and there’s a common pattern. People know enough to get by, but they’re missing the fundamentals that would make their lives easier. They’re running containers without health checks. They’re building 2GB images when they could be 50MB. They’re not using multi-stage builds, or security scanning their images, or understanding how layer caching saves build time and data transfer costs.</p>

<p>You might know how to <code class="language-plaintext highlighter-rouge">docker build</code> and <code class="language-plaintext highlighter-rouge">docker run</code>, but do you really understand Docker volumes and why data in containers isn’t permanent? Can you configure application settings across different environments without rebuilding images? Do you know how containers enable advanced patterns like HTTP traffic management with reverse proxies or asynchronous messaging with queues?</p>

<p alt="Diagram showing asynchronous messaging architecture using Docker containers with message queues connecting multiple services for event-driven communication patterns"><img src="/content/images/2025/10/diamol-async.png" alt="Async messaging with containers" /></p>

<p>Learn Docker in a Month of Lunches (Second Edition) has got you covered. It walks you through Docker with a practical hands-on approach, giving you experience in everything from the fundamentals to image optimization and cross-platform delivery. But you don’t have to follow the journey - every chapter is independent. Already comfortable with basic Dockerfiles? Jump straight to Chapter 17 on optimizing images for size, speed, and security. Need to understand networking? Chapter 7 walks through Docker Compose and how Docker plugs containers together. Want to finally master volumes on Windows AND Linux? Chapter 6 has you covered.</p>

<h2 id="whats-new-in-the-second-edition">What’s New in the Second Edition</h2>

<p>The first edition came out in 2021, and although the core concepts haven’t changed the book content is new with every exercise rewritten and tested for the latest releases. Everything works cross-platform: Linux, Windows, Intel, and ARM. You can follow along on your Apple Silicon, your Windows 11 laptop, or a Ubuntu server you’re running in the cloud. There’s a whole chapter on replatforming legacy Windows apps - because yes, those old .NET Framework applications deserve a new home in containers.</p>

<p>The runtime chapters of the book are a complete refresh, covering all the options you have to run containers in production. Azure Container Apps and Google Cloud Run for serverless containers in the cloud, a primer on Kubernetes, and GitHub Actions for CI/CD.</p>

<h2 id="from-basics-to-production">From Basics to Production</h2>

<p>The book’s structured to take you from zero to production-ready. Part 1 covers the fundamentals - understanding containers and images, building multi-stage Dockerfiles for Java, Node.js, and Go apps, and sharing images through registries.</p>

<p>Part 2 gets into the real-world stuff: running distributed applications with Docker Compose, implementing health checks and dependency checks, adding observability with Prometheus and Grafana, and building a proper CI/CD pipeline that only needs Docker.</p>

<p>Part 3 shows you how to run containers anywhere - multi-platform builds that work on ARM and Intel, managed container services in Azure and Google Cloud, and yes, Kubernetes.</p>

<p alt="Screenshot of a Kubernetes cluster running on multiple platforms including ARM and Intel architectures with Linux and Windows nodes deployed across different cloud environments"><img src="/content/images/2025/10/diamol-k8s-cluster.png" alt="A multi-platform Kubernetes cluster" /></p>

<p>Part 4 is where it gets really interesting - production patterns like configuration management, centralized logging, reverse proxies for traffic control, and message queues for asynchronous communication.</p>

<h2 id="the-practical-approach">The Practical Approach</h2>

<p>Each chapter is a “lunch” - about an hour of focused learning that you can actually complete in a lunch break.</p>

<p>Every topic is grounded in real problems I’ve seen teams struggle with. Application configuration management across environments? Chapter 18. Writing and managing logs properly? Chapter 19. Getting containers production-ready with proper optimization? Chapter 17. These aren’t theoretical exercises - they’re solutions to actual problems you’ll face.</p>

<h2 id="getting-started">Getting Started</h2>

<p>The second edition of Learn Docker in a Month of Lunches is available now from <a href="https://www.manning.com/books/learn-docker-in-a-month-of-lunches-second-edition">Manning</a> and another book-selling website called <a href="https://www.amazon.com//dp/1633438465">Amazon</a>. Whether you’re fixing those knowledge gaps or starting fresh, it’s the practical guide to Docker that focuses on what you actually need to know to be productive.</p>

<p>Docker might be ubiquitous in 2025, but that doesn’t mean everyone’s using it well. This book helps you join the group that is.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="docker" /><category term="learning" /><category term="books" /><category term="containers" /><summary type="html"><![CDATA[Docker is established tech now - so why would anyone buy a book about it? Because most people only learn a fraction of what Docker can do from their day job, and it pays to learn it all.]]></summary></entry><entry><title type="html">My New SRE Course: Resiliency and Automation (2025)</title><link href="https://blog.sixeyed.com/sre-resiliency-course/" rel="alternate" type="text/html" title="My New SRE Course: Resiliency and Automation (2025)" /><published>2025-08-18T09:00:00+00:00</published><updated>2025-08-18T09:00:00+00:00</updated><id>https://blog.sixeyed.com/sre-resiliency-course</id><content type="html" xml:base="https://blog.sixeyed.com/sre-resiliency-course/"><![CDATA[<h1 id="my-new-sre-course-resiliency-and-automation-">My New SRE Course: Resiliency and Automation 🚀</h1>

<p>My latest Pluralsight course is out!</p>

<p class="notice--info"><a href="/l/ps-sre-resiliency">SRE: Resiliency and Automation</a></p>

<p>It’s the third course in the <a href="/l/ps-sre-path">Site Reliability Engineering learning path</a>, and it’s all about building systems that survive the chaos of production.</p>

<p>This course came from a simple observation: most teams think their systems are reliable because they work perfectly in test environments. But production is hostile. Pods crash, nodes fail, dependencies timeout, and cloud services have outages. The question isn’t whether these failures will happen - it’s whether your system will survive them. 💪</p>

<h2 id="sre-resiliency-the-story-">SRE Resiliency: The Story 📖</h2>

<p>The course follows an SRE team that’s had enough. They’re handing back the pager to the development team because the application is consuming their entire toil budget with constant incidents. But this isn’t about blame - it’s about partnership. The SRE team walks the developers through exactly what needs to change before they’ll take operational responsibility back.</p>

<p alt="SRE team handing back the pager to the development team"><img src="/content/images/2025/08/sre-hand-back-pager.png" alt="SRE team handing back the pager" /></p>

<p>We use this narrative to explore the core practices that transform hope-based reliability into evidence-based confidence. You’ll follow two fictional SREs, Carlos and Keiko, as they help steer the app to production reliability. You’ll see Carlos demonstrating the problems with traditional approaches, then Keiko showing how SRE teams solve these issues at scale.</p>

<h2 id="sre-skills-youll-master">SRE Skills You’ll Master</h2>

<p>The course covers five essential areas that every production system needs:</p>

<p><strong>Architectural Resilience</strong> - You’ll see why synchronous architectures create operational nightmares and how patterns like distributed caching and async messaging provide the graceful degradation that production demands. We take an app that’s failing under normal load and transform it into something that maintains its SLOs.</p>

<p><strong>GitOps and Automation</strong> - Manual deployments don’t scale to multiple releases per day. You’ll learn how Infrastructure as Code with Terraform, application modeling with Helm, and continuous reconciliation with <a href="https://argo-cd.readthedocs.io/">ArgoCD</a> create self-healing systems that fix themselves at 3 AM while you sleep. 😴</p>

<p alt="GitOps workflow diagram showing Infrastructure as Code with Terraform, Helm charts, and ArgoCD continuous reconciliation for automated deployments"><img src="/content/images/2025/08/gitops-argocd-workflow.png" alt="GitOps workflow with ArgoCD" /></p>

<p><strong>Capacity Planning and Autoscaling</strong> - Pre-production sizing is guesswork. The course shows how to build systems that discover their own capacity needs through horizontal pod autoscaling, cluster autoscaling, and <a href="https://keda.sh/">KEDA</a>. Start small, measure everything, and let reality drive your scaling. 📊</p>

<p><strong>Chaos Engineering</strong> - Perfect test environments create dangerous blind spots. You’ll see how to use <a href="https://chaos-mesh.org/">Chaos Mesh</a> to deliberately break things, proving your system can handle pod failures, node crashes, and dependency outages before they happen in production. 🔨</p>

<p alt="Chaos Mesh dashboard showing chaos engineering experiments including pod failures, node crashes, and network latency injection for testing system resilience"><img src="/content/images/2025/08/chaos-mesh-experiments.png" alt="Chaos engineering with Chaos Mesh" /></p>

<p><strong>Disaster Recovery</strong> - Even the most resilient system can’t survive everything. The final module covers how SRE teams classify applications by business criticality and implement appropriate DR strategies for regional failures.</p>

<h2 id="real-problems-real-solutions-">Real Problems, Real Solutions 🎯</h2>

<p>Every demo in the course reproduces actual production problems. When you see timeouts, cascading failures, and manual deployment disasters, these aren’t theoretical examples - they’re recreations of the issues that force SRE teams to hand back the pager.</p>

<p>The solutions aren’t exotic either. These are the standard infrastructure patterns that emerge from running hundreds of services at scale. Distributed caching with <a href="https://redis.io/">Redis</a>, message queuing for async processing, GitOps with ArgoCD - the tools and techniques that working SRE teams use every day.</p>

<h2 id="target-audience-for-sre-professionals">Target Audience for SRE Professionals</h2>

<p>This course is perfect if you’re:</p>

<ul>
  <li>A developer working with SRE teams who wants to understand their requirements</li>
  <li>An operations engineer looking to move into SRE</li>
  <li>An architect designing systems that need to run reliably at scale</li>
</ul>

<p>You’ll need basic knowledge of distributed systems and cloud platforms, plus an understanding of SRE fundamentals from <a href="https://blog.sixeyed.com/sre-learning-path-pluralsight/">the earlier courses in the SRE learning path</a>. The demo application runs in Kubernetes, but you don’t need to be an expert - the principles and approaches are the key things you’ll learn here, not just the technology implementation.</p>

<h2 id="the-sre-partnership-model-">The SRE Partnership Model 🤝</h2>

<p>One thing I really wanted to emphasize in this course is that SRE isn’t about one team imposing rules on another. It’s about partnership. Development teams bring deep application knowledge and feature expertise. SRE teams bring operational experience from running systems at scale. Together, they build something neither could achieve alone.</p>

<p>When the SRE team hands back the pager in module one, it’s not a failure - it’s a recognition that the application needs architectural changes that only the dev team can implement. When they take it back after the improvements, both teams win. Developers get faster deployments and more autonomy. SRE teams get sustainable operations with manageable toil.</p>

<p alt="SRE partnership model diagram illustrating collaboration between development teams and SRE teams, showing shared responsibilities for application reliability and operational excellence"><img src="/content/images/2025/08/sre-team-collaboration.png" alt="SRE partnership model" /></p>

<h2 id="next-steps-️">Next Steps ➡️</h2>

<p><a href="/l/ps-sre-resiliency">SRE: Resiliency and Automation</a> is available now on Pluralsight. It’s about 90 minutes of content split across five modules, each with practical demos you can follow along with.</p>

<p>If you haven’t started the SRE learning path yet, begin with <a href="/l/ps-sre-concepts">SRE: Concepts and Principles</a> for the fundamentals, then move through the path to build your expertise.</p>

<p>The SRE approach transforms how we build and run systems. Instead of hoping things won’t break, we prove they can survive. Instead of firefighting the same issues repeatedly, we build systems that heal themselves. It’s a better way to work for everyone - developers, operators, and especially the users who depend on our services. 🎉</p>

<p>Happy learning!</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="sre" /><category term="pluralsight" /><category term="kubernetes" /><category term="devops" /><category term="resiliency" /><category term="automation" /><category term="gitops" /><category term="chaos-engineering" /><category term="disaster-recovery" /><category term="argocd" /><summary type="html"><![CDATA[Learn how SRE teams build resilient production systems with my new Pluralsight course. Master GitOps, chaos engineering, automation patterns, and disaster recovery strategies that survive production chaos.]]></summary></entry><entry><title type="html">10 Essential Claude Code Tips: Boost Your AI Coding Productivity in 2025</title><link href="https://blog.sixeyed.com/ten-tips-claude-code/" rel="alternate" type="text/html" title="10 Essential Claude Code Tips: Boost Your AI Coding Productivity in 2025" /><published>2025-07-15T09:00:00+00:00</published><updated>2025-07-15T09:00:00+00:00</updated><id>https://blog.sixeyed.com/ten-tips-claude-code</id><content type="html" xml:base="https://blog.sixeyed.com/ten-tips-claude-code/"><![CDATA[<p><a href="https://www.anthropic.com/claude-code">Claude Code</a> is Anthropic’s agentic coding tool that transforms AI pair programming. It lets you delegate development tasks directly from your VS Code terminal - you describe what you want, and a team of Claudes build it while you focus on the bigger picture.</p>

<p>My journey with Claude Code went like this:</p>
<ul>
  <li><em>mildly skeptical</em> 🤔</li>
  <li><em>pleasantly surprised</em> 😯</li>
  <li><em>thoroughly impressed</em> 🤯</li>
  <li><em>cannot live without</em> 🚀</li>
</ul>

<p>This Claude Code tutorial covers 10 battle-tested tips from real projects that will help you work like a tech lead with an AI development team at your command.</p>

<blockquote>
  <p><strong>Quick Summary</strong>: Claude Code transforms you from a coder into a development director. These 10 Claude Code best practices will help you manage multiple AI coding agents, maintain code quality, and dramatically increase productivity. Time required: 5 minutes to read, hours of new free time to fill.</p>
</blockquote>

<h2 id="getting-started-with-claude-code">Getting Started with Claude Code</h2>

<p>Setting up is straightforward: <a href="https://claude.ai/login">create a free account</a>, install the <a href="https://docs.anthropic.com/en/docs/claude-code/ide-integrations">Claude Code extension in VS Code</a>, authenticate and you’re ready. Open a terminal, type <code class="language-plaintext highlighter-rouge">claude</code> and start describing what you need. The real power comes from understanding how to work with it effectively.</p>

<blockquote>
  <p>I used Claude Code to build an entire <a href="https://github.com/sixeyed/multi-cloud-demo">multi-cloud AKS/EKS demo application</a>. With a few hours of guidance, Claude completed what would have taken me at least 3 days to write myself.</p>
</blockquote>

<h2 id="1-run-multiple-claude-instances-multitask-like-a-manager">1. Run Multiple Claude Instances: Multitask Like a Manager</h2>

<p>Run multiple Claude instances across different terminal sessions. While one’s building your API endpoints, another can work on the frontend, and a third can write your deployment scripts. Switch between them to provide guidance - it’s like having a team of developers who are extremely eager and who know <em>everything</em>.</p>

<p>Make sure you have plenty of things on the go - work, pet projects, blogs, tech explorations. And don’t be afraid to let it loop - prompts like “keep iterating on the build: fix any issues with the terraform config and deployment scripts, run the script, watch the outcome and repeat until it works” will keep Claude busy.</p>

<p class="notice--info">It’s the new ABC: <strong>Always Be Clauding</strong>.</p>

<h2 id="2-delegate-debugging-let-claude-do-the-work">2. Delegate Debugging: Let Claude Do the Work</h2>

<p>When something’s broken, resist the urge to fix it yourself. That’s not why you’re here. Describe the problem and let Claude handle the implementation. If you’re diving into the code to make changes, you’re going too deep. Stay at the design level where you add the most value.</p>

<p>Claude will use all the same debugging tools you use to find issues (it asks permission first and stores the permissions you’ve granted). If you see an error log, just give it to Claude and it will use <code class="language-plaintext highlighter-rouge">kubectl</code> to examine your Pods and Services, <code class="language-plaintext highlighter-rouge">curl</code> to test endpoints, <code class="language-plaintext highlighter-rouge">nslookup</code> for DNS queries and so on.</p>

<h2 id="3-code-review-mindset-roll-with-ai-generated-code">3. Code Review Mindset: Roll With AI-Generated Code</h2>

<p>Claude’s code isn’t going to look like yours. That’s fine. Treat it like you’re reviewing someone’s PR - does it meet the requirements? Is it maintainable? If you have standards, enforce them in the repo. Don’t get hung up on style differences. The goal is working software, not perfect alignment with your own preferences.</p>

<h2 id="4-rapid-prototyping-design-and-iterate-on-the-fly">4. Rapid Prototyping: Design and Iterate on the Fly</h2>

<p>Coding is cheap now. Really cheap. Need to refactor the entire architecture? Just ask. Want to switch from REST to GraphQL? Claude can handle it. Don’t overthink the initial design - build something that works, then iterate. It’s liberating when a complete redesign takes minutes, not days.</p>

<h2 id="5-git-best-practices-stay-in-control-of-commits">5. Git Best Practices: Stay in Control of Commits</h2>

<p>Claude can commit code and write commit messages, but don’t let it run on autopilot. Review the diffs, commit frequently, and keep your Git history clean. You want to understand what’s changing - that’s how you maintain ownership of the codebase.</p>

<p>Ready to try this? <a href="https://www.anthropic.com/claude-code">Start with Claude Code free</a> and experience the power of AI-assisted development.</p>

<h2 id="6-beyond-application-code-let-claude-handle-infrastructure">6. Beyond Application Code: Let Claude Handle Infrastructure</h2>

<p>Don’t just use it for application code. Claude can write your <a href="/learn-docker-in-a-month-of-lunches/">Dockerfiles</a>, <a href="/learn-kubernetes-in-a-month-of-lunches/">Kubernetes</a> manifests, <a href="https://www.terraform.io/">Terraform</a> configs, CI/CD pipelines, test suites, documentation. It will even give you guidance on architecture and tech stack. Push the boundaries - you’ll be surprised at what it can do. It has memorized the entire Internet, after all (probably).</p>

<h2 id="7-troubleshooting-complex-tasks-be-persistent">7. Troubleshooting Complex Tasks: Be Persistent</h2>

<p>Some tasks are harder for Claude than others. I’ve had situations where it took a dozen prompts to get a local LGTM stack running, or to authenticate to a new EKS cluster. When it struggles, approach from different angles. Rephrase your requirements, break complex tasks into steps, feed in error messages, or provide examples.  Like any team member, Claude sometimes needs extra guidance to get unstuck.</p>

<p>Unlike most team members, Claude sometimes gives up. It will say something like “success! I’ve got it all working except the things you really wanted”. But that doesn’t mean it can’t do it, it’s just reached the end of the road for that prompt - try again.</p>

<h2 id="8-claudemd-best-practices-provide-context-upfront">8. CLAUDE.md Best Practices: Provide Context Upfront</h2>

<p>Create a <a href="https://docs.anthropic.com/en/docs/claude-code/memory"><code class="language-plaintext highlighter-rouge">CLAUDE.md</code> file</a> in your project root. This is where Claude documents everything it needs to know - architecture decisions, tech preferences, naming conventions, project structure, and coding standards. Claude Code automatically reads this at the start of each session, so you don’t need to repeat yourself.</p>

<p>A good <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> is like a comprehensive onboarding doc for a new developer - and you can ask Claude to update it at the end of a session with new learnings, which it will pick up next time. Here’s what it looks like - create it with the <code class="language-plaintext highlighter-rouge">/init</code> command when you bring Claude onto the project and keep it current with prompts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># CLAUDE.md - AI Assistant Memory File

## Project Overview
This is a multi-cloud Kubernetes demo application showcasing how containerized .NET applications can be deployed consistently across different cloud providers. The application demonstrates modern microservices patterns, message queuing, database persistence, and Kubernetes deployment best practices.

## Architecture

### Components
- **WebApp**: ASP.NET Core web application with Razor Pages
  - Form for message submission
  - Messages page displaying processed data from SQL Server
  - Modern gradient UI design with 3rem font sizes
  - Antiforgery tokens disabled for demo simplicity
</code></pre></div></div>

<h2 id="9-async-work-queue-batch-your-changes">9. Async Work Queue: Batch Your Changes</h2>

<p>While Claude is working on a longer task, queue up your next prompts. If you know you’ll need API tests after the endpoints are done, type that prompt and hit enter - Claude will pick it up when ready. This keeps Claude productive while you check on your other instances. It’s like having a queue of work that you can fill ahead of time.</p>

<h2 id="10-knowledge-management-capture-and-reuse-prompts">10. Knowledge Management: Capture and Reuse Prompts</h2>

<p>Ask Claude to dump all the prompts from your session to a text file in the repo. It’s incredibly useful to see how the development evolved - what worked, what needed clarification, how you refined requirements. These prompt histories become scaffolding for your next similar project. You’ll build up a library of effective prompts that you can reuse and adapt.</p>

<h2 id="claude-code-pricing-and-usage-limits">Claude Code Pricing and Usage Limits</h2>

<p>Don’t get too hung up on the details - which model you’re using or which plan you’re on. I use the more advanced <a href="https://docs.anthropic.com/en/docs/claude-code/settings#model-selection">Opus 4 model</a> by default, but Claude automatically switches to Sonnet 4 when you’re running low on credits, and it’s perfectly capable.</p>

<p>The <a href="https://docs.anthropic.com/en/docs/claude-code/costs">usage restrictions</a> are very fair - when you hit the limits, they reset after a period. More expensive plans have higher limits and shorter reset periods. Just focus on being productive with whatever you have.</p>

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>Q: Is Claude Code free to use?</strong><br />
A: Claude Code offers a free tier with limited usage. Paid plans provide higher limits and access to more powerful models like Opus 4.</p>

<p><strong>Q: Does Claude Code work with any language?</strong><br />
A: Yes! Claude Code supports all major programming languages including Python, Java, C#, Go, Rust, and more.</p>

<p><strong>Q: Can Claude Code work with existing codebases?</strong><br />
A: Absolutely. Claude Code can analyze and modify existing code. The CLAUDE.md file helps it understand your project structure and conventions.</p>

<p><strong>Q: How does Claude Code compare to GitHub Copilot?</strong><br />
A: While Copilot offers inline suggestions, Claude Code works at a higher level - managing entire features and projects through conversation. It’s more like having an AI pair programming partner who can handle complex, multi-file tasks.</p>

<p><strong>Q: Can I use Claude Code for production applications?</strong><br />
A: Yes, but always review Claude’s code thoroughly. Treat it like any code review - check for security issues, performance concerns, and adherence to your coding standards.</p>

<h2 id="the-future-of-ai-assisted-development">The Future of AI-Assisted Development</h2>

<p>Claude Code fundamentally changes how we write software. Instead of coding, you’re directing. Instead of debugging syntax, you’re validating solutions. Embrace this new way of working - you will suddenly become hugely more productive.</p>

<p>I imagine the next step will be a higher level still - you’ll plug Claude into your product backlog and set X number of instances running to do the entire project. One Claude will test and review the work of another Claude, and maybe there will be a manager (the Claude of Claudes) who takes over the director role.</p>

<p>But for now, you are the director. If you’re ready to transform your development workflow, <a href="https://www.anthropic.com/claude-code">get started with Claude Code</a> and experience the future of AI-powered coding. For a more detailed analysis of what multiple Claudes can do, check out my post <a href="/claude-is-coming-for-your-job/">An Evening with Claude Code - or - How I Learned to Stop Worrying and Love AI</a>.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="ai" /><category term="claude" /><category term="productivity" /><category term="development" /><category term="claude-code" /><category term="ai-coding" /><category term="developer-tools" /><category term="ai-pair-programming" /><category term="prompt-engineering" /><category term="vs-code-extensions" /><category term="developer-productivity" /><summary type="html"><![CDATA[Master Claude Code with 10 battle-tested tips from real projects. Learn to run multiple AI agents, delegate effectively, and 10x your dev productivity in 2025.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2025/07/claude-code-hero.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2025/07/claude-code-hero.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">An Evening with Claude Code - or - How I Learned to Stop Worrying and Love AI</title><link href="https://blog.sixeyed.com/claude-is-coming-for-your-job/" rel="alternate" type="text/html" title="An Evening with Claude Code - or - How I Learned to Stop Worrying and Love AI" /><published>2025-07-10T09:00:00+00:00</published><updated>2025-07-10T09:00:00+00:00</updated><id>https://blog.sixeyed.com/claude-is-coming-for-your-job</id><content type="html" xml:base="https://blog.sixeyed.com/claude-is-coming-for-your-job/"><![CDATA[<p>It’s 7pm, Friday night and I’m working on three different projects simultaneously with my new favorite colleague: <a href="https://www.anthropic.com/claude-code">Claude Code</a>.</p>

<ul>
  <li>Claude #1 is building a multi-cloud proof-of-concept for a client.</li>
  <li>Claude #2 is creating demos for my next Pluralsight course</li>
  <li>Claude #3 is fixing the UI issues on this blog that I’ve ignored for years.</li>
</ul>

<p>Actually… I’m mostly watching <a href="https://www.imdb.com/title/tt3606756/">Incredibles 2</a> with my kids, and just checking in on each of the Claudes in turn to nudge them to their next step. This is AI coding today.</p>

<h2 id="welcome-to-the-paradigm-shift">Welcome to the Paradigm Shift</h2>

<p>We - engineers and architects - shouldn’t feel we’re competing with AI. 🎬 Our role is to direct it. Bandwidth is no longer the limit, because we can distribute work to as many AI agents as we can manage. With multiple Claudes I can productively work on multiple tasks in parallel.</p>

<p>The mythical 10X developer turns out to be a regular 1X developer directing 10 instances of Claude Code. Even the best multitasking developers pay a cost every time they context switch, but AI doesn’t have that overhead. Each instance maintains its full context, while you operate on a higher level checking in and guiding them all.</p>

<blockquote>
  <p>I’ve heard the same joke from my consultancy clients for years - they want to clone me so they can run me at scale. It feels like Anthropic are doing that with Claude Code.</p>
</blockquote>

<p>I’ve been using Claude Code more and more, and this parallel workflow is a real breakthrough. This post covers what I think where AI is going in the short term: not replacing developers, but fundamentally changing what engineering roles look like. One person running multiple AI agents is like a tech lead with a hugely knowledgeable, experienced and dedicated team. 🚀 So yes, AI is coming for your job - not to take it from you, but to transform it into something entirely more awesome.</p>

<h2 id="project-1-the-cloud-proof-of-concept">Project #1: The Cloud Proof-of-Concept</h2>

<p>I have a consulting client who are multi-cloud, but the area I work in is 100% Azure. They’re looking at broadening that to include AWS but they’re skeptical about how easy it is to migrate apps between clouds.</p>

<p>🐳 For years I’ve been saying that Docker and Kubernetes are the keystones of portable apps. They wanted a generic, simple proof of concept they could use to see if that was true, and to diff the Azure and AWS setup. I thought that was something Claude could help me with.</p>

<blockquote>
  <p>The full code Claude generated is on GitHub: <a href="https://github.com/sixeyed/multi-cloud-demo">sixeyed/multi-cloud-demo</a>. Here’s a snap of the app running in Azure with fully automated deployments built by Claude:</p>
</blockquote>

<p><img src="/content/images/2025/07/claude-azure-demo.png" alt="Multi-cloud demo application running in Azure showing distributed system with frontend, Redis queue, and SQL Server database deployed by Claude Code" /></p>

<p>This is how the conversation with Claude Code started - using the <a href="https://docs.anthropic.com/en/docs/claude-code/ide-integrations#installation">VS Code integration</a> in an empty folder:</p>

<div class="prompt-wrap">1. "this is a new simple demo app for showing how Kubernetes deployments can work the same way in different clouds. i'd like to create a basic multi service application - a web app which posts text to a redis queue, and a background worker which reads from the queue. both should be .NET, and the web app should have a very simple form for the user to enter text. i'd also like docker files and docker compose.yml so this runs locally for development"

2. "great. now let's have a helm folder with a chart to deploy this app to kubernetes. we'll want the chart to have a redis dependency - probably bitnami's chart"</div>

<p>At this point I had the source code, Docker and Kubernetes artifacts for a working demo. Then it gets interesting because I’m designing this out loud and Claude is reacting to changes in requirements:</p>

<div class="prompt-wrap">3. "now i want to demonstrate different kubernetes features. can we add to the worker process so it writes logs to a file or persists data somewhere so we can see PVCs and different storage options"

4. "no, scratch that. leave the logging to console but also add SQL server container to docker-compose.yml and have the worker write the messages to a database table"</div>

<p>And now I have a database defined in my Docker Compose spec, with the source code extended to include persistent storage. Claude also added it to the Kubernetes model without my asking, because by now it had enough context to know we’d be using both.</p>

<p>This is impressive enough, but the output isn’t perfect and - like any developer - Claude does get things wrong. What’s <em>really</em> impressive is that you can tell Claude there’s an issue, and it will use the same tools you would to debug, track down the root cause and fix it:</p>

<div class="prompt-wrap">5. "the data isn't getting into sql server from the worker. check the connection string and the ef core code"</div>

<p>That triggered lots of approval requests so Claude could use tools like <code class="language-plaintext highlighter-rouge">kubectl</code> and <code class="language-plaintext highlighter-rouge">curl</code> - nothing happens without your permission. It found the problem, fixed the Kubernetes specs and we were off again.</p>

<p>🤖 Generative AI for code is like having an engineer on the team who’s extremely knowledgeable and very enthusiastic, but lacking experience. Your role is to guide them, feed them tasks which you break down into sensible steps and describe clearly, and give feedback when something needs more work.</p>

<p>Some of these prompts take Claude a minute or two to work on, sometimes longer. That’s when you - as the AI team lead - switch to another instance on another part of the project, or a different project entirely. That other Claude has finished its latest task so you guide it on to the next one.</p>

<p>I’m always polite with Claude because of <a href="https://www.imdb.com/title/tt0103064/">Terminator 2</a>, but it doesn’t mind criticism. Sometimes it approaches tasks in an odd way, and you just point out what you’d prefer and it goes off and corrects it:</p>

<div class="prompt-wrap">36. "this is very cool. let's add another page to the web app which shows the messages in sql server. probably best to split it out so the html isn't all in a string now too :)"

37. "also - why do we have HTML strings at all? shouldn't we be using razor pages or something."</div>

<p>You get the idea. The <a href="https://github.com/sixeyed/multi-cloud-demo/blob/main/user-prompts.txt">full conversation</a> ran to 90 prompts, and at the end I had a full stack repo with Terraform configurations to create the Azure and AWS infrastructure, Helm charts to deploy the same app to both, and detailed documentation.</p>

<blockquote>
  <p>I made a point of not touching any code myself. Anything that didn’t work or needed changing went into a prompt. So Claude did it all - but Claude couldn’t have done it without me.</p>
</blockquote>

<p>This is why there’s still a place for human engineers. Maybe not for long, but for now the AI needs guidance. The more knowledge and experience you bring as the human guide, the more productive the AI can be. Rough guess I think this would have taken one human about 3 days to write; with Claude it was done in a few hours of intermittent guidance.</p>

<h2 id="project-2-pluralsight-course-demo">Project #2: Pluralsight Course Demo</h2>

<p>And while that goes on there, look at this going on here. In a different VS Code window I have a separate Claude Code session.</p>

<p>I’m working on a new <a href="">SRE learning path for Pluralsight</a>. There are four courses and I like to have a different demo app for each course. Those apps are built to show specific behavior to highlight how SRE approaches and tools can fix issues.</p>

<p>In the old days I might spend the first week of a new course building the demo app. I quite enjoyed that - it’s responsibility-free coding because the app will never run in anger - and it gave me a chance to use all the latest tech stacks and keep up to date.</p>

<p>But actually it’s not a very effective use of time. Far better to get Claude to write that throwaway code for me, freeing up my time to work on the content.</p>

<p>After a few iterations Claude had built a demo app which could be configured to deliberately fail in interesting ways, with a full GitOps stack to create Azure infrastructure with Terraform and deploy to Kubernetes with Argo.</p>

<p>Here’s part of Claude’s summary for that session, running while the other Claude was building my multi-cloud demo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## Final State
Repository is now:
- ✅ PowerShell standardized
- ✅ Self-bootstrapping infrastructure  
- ✅ Simplified testing approach
- ✅ Zero-configuration user experience
- ✅ Production-ready GitOps setup

## Key Takeaways
1. Complex test frameworks can become dead ends - simpler is often better
2. Self-bootstrapping infrastructure eliminates user setup pain
3. Consistency (PowerShell only) reduces maintenance overhead
4. Real infrastructure validation &gt; mocked tests for reliability demos
</code></pre></div></div>

<p>This was another app which started from scratch. ⏰ I saved a week of tinkering and could get straight onto the content for the course. It takes a little bit of introspection to admit: <em>this task is better suited to Claude than to me</em> . But it is and it means that course is getting to completion more quickly.</p>

<h2 id="project-3-the-blog-ui-finally">Project #3: The Blog UI (Finally!)</h2>

<p>And the last thing Claude was helping me with was this blog. I’ve always focused on the content and have pretty much zero interest in HTML and CSS. Over the years I’ve used different frameworks and platforms, the current setup is Jekyll powered by GitHub pages.</p>

<p>The theme is a modification of <a href="https://mmistakes.github.io/minimal-mistakes/">Minimal Mistakes</a> and the mobile experience has always sucked, but it’s one of those things I never fancied working on.</p>

<p>So in my third session I fired up Claude and introduced it to the blog repo. With the <code class="language-plaintext highlighter-rouge">init</code> command it inspected the source code and generated the <a href="https://docs.anthropic.com/en/docs/claude-code/memory">CLAUDE.md</a> document for its own guidance, including a high level overview:</p>

<div class="prompt-wrap">## Project Overview

This is a Jekyll-based blog using the Minimal Mistakes theme, hosted on GitHub Pages. The blog belongs to Elton Stoneman, a freelance IT consultant and trainer.</div>

<p>While the other two Claudes were working on their own things, I had this Claude fixing up the responsive design, adding SEO-metadata to recent posts and generally tidying things up.</p>

<p>I even got Claude to write a url-shortening component, to make it easier to control links. So my Pluralsight author page is available through my blog at https://blog.sixeyed.com/l/ps-home.</p>

<p>Years of technical debt fixed while my other projects built themselves. 📱 The blog finally looks professional on mobile. You can actually read my posts on your phone without zooming and scrolling horizontally.</p>

<p><img src="/content/images/2025/07/claude-blog.png" alt="Responsive blog design showing mobile-optimized layout with proper text wrapping and improved user experience created by Claude Code" /></p>

<h2 id="the-evenings-tally">The Evening’s Tally</h2>

<p>Final check-in across all three terminals:</p>
<ul>
  <li><strong>Client POC</strong>: Full distributed system deployed on both AKS and EKS - frontend accepting jobs, Redis queuing them, workers processing, results in SQL</li>
  <li><strong>Course demos</strong>: Six architectural patterns, fully containerized with documentation</li>
  <li><strong>Blog</strong>: Responsive, dark mode enabled, finally entering the 2020s</li>
</ul>

<p>✨ I accomplished three different project milestones in one evening (and a little bit into the following morning). Not by working faster - by working on multiple things simultaneously.</p>

<h2 id="the-realization">The Realization</h2>

<p>💪 What I’ve realized is that the value of human oversight across multiple AI workers is the new superpower. You become the tech lead doing rounds, checking on your team’s progress, providing direction, ensuring quality.</p>

<p>Here’s the hardest habit to break: <em>the urge to jump in and edit code manually</em>. Every time I see a small bug or want to tweak something, muscle memory says “I’ll just quickly fix this.” But that’s the old way. It’s always more effective to describe the change to Claude and move on to push forward another project. Let the AI make the change while you’re being productive elsewhere. Breaking this habit is like learning to delegate - uncomfortable at first, but essential for scaling.</p>

<h2 id="the-competitive-reality">The Competitive Reality</h2>

<p>💯 Here’s the uncomfortable truth: one AI-enabled developer can now deliver what used to take a small team. Not because AI is better at coding than humans, but because one human can effectively direct multiple AI developers working in parallel.</p>

<p>The good news? If you adapt, you become incredibly valuable. The concerning news? If you’re still working sequentially, you’re competing against people running parallel workstreams.</p>

<p>My advice:</p>

<ul>
  <li>Start thinking in parallel projects (or at least parallel tasks in the same project), not sequential tasks</li>
  <li>Get comfortable being a reviewer/director/tester/product manager rather than an implementer</li>
  <li>Practice managing multiple contexts simultaneously</li>
  <li>Focus on the skills AI can’t replicate: understanding business value, making architectural decisions, ensuring quality</li>
</ul>

<p class="notice--info">🎬 Think of yourself as a director, managing all the talent. You couldn’t do it without them - but they couldn’t do it without you either.</p>

<p>The code from all three projects is on GitHub. And the entire Claude Code transcript for the multi-cloud demo app is there too - every prompt. You can see exactly how a distributed system went from nothing to multi-cloud deployment without me writing a single line of code.</p>

<p>Stop thinking about AI as just a faster way to code, or maybe a threat to your job. Start thinking about it as your development team.</p>

<p>Now if you’ll excuse me, I’ve got a few more Claude Code instances to spin up. My next Pluralsight course demo isn’t going to build itself. 😏 Well, actually…</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="ai" /><category term="claude" /><category term="claude-code" /><category term="productivity" /><category term="development" /><category term="automation" /><category term="parallel-programming" /><category term="artificial-intelligence" /><category term="software-engineering" /><summary type="html"><![CDATA[One evening running three parallel development projects with Claude Code - building a client POC, creating course demos, and fixing blog UI issues simultaneously. AI isn't replacing developers, it's transforming us into directors of multiple AI workers.]]></summary></entry><entry><title type="html">Site Reliability Engineering (SRE) on Pluralsight: Complete 4-Course Learning Path</title><link href="https://blog.sixeyed.com/sre-learning-path-pluralsight/" rel="alternate" type="text/html" title="Site Reliability Engineering (SRE) on Pluralsight: Complete 4-Course Learning Path" /><published>2025-07-06T10:00:00+00:00</published><updated>2025-07-06T10:00:00+00:00</updated><id>https://blog.sixeyed.com/sre-learning-path-pluralsight</id><content type="html" xml:base="https://blog.sixeyed.com/sre-learning-path-pluralsight/"><![CDATA[<p><a href="https://sre.google/sre-book/table-of-contents/">Site Reliability Engineering</a> is how Google runs production systems, and it’s becoming the standard approach for managing complex applications at scale. I’ve just published the first two courses in a new <a href="/l/ps-sre-path">SRE learning path on Pluralsight</a>, with two more courses coming soon to complete the path.</p>

<p>SRE achieves the same goals as <a href="https://www.atlassian.com/devops">DevOps</a> - high availability with high velocity - but without requiring a massive culture shift. It’s an engineering approach to operations that focuses on automation, measurement, and removing toil. For many organizations starting their digital transformation, SRE provides a more structured path forward than traditional DevOps adoption.</p>

<p class="notice--info">I cover the easy(ish) way to add reliability at scale with container orchestration in my 5* Pluralsight course <a href="/l/ps-istio">Managing Apps on Kubernetes with Istio</a>.</p>

<h2 id="the-sre-learning-path">The SRE Learning Path</h2>

<p>The complete <a href="/l/ps-sre-path">Site Reliability Engineering learning path</a> takes you from SRE fundamentals through to advanced practices:</p>

<ol>
  <li><a href="/l/ps-sre-concepts">SRE: Concepts and Principles</a></li>
  <li><a href="/l/ps-sre-monitoring">SRE: Monitoring and Observability</a></li>
  <li><a href="/l/ps-sre-resiliency">SRE: Resiliency and Automation</a></li>
  <li>SRE: Measuring and Optimizing Reliability <em>(coming soon)</em></li>
</ol>

<p>Let’s dive into what’s covered in the first two courses.</p>

<h2 id="course-1-sre-concepts-and-principles">Course 1: SRE Concepts and Principles</h2>

<p><a href="/l/ps-sre-concepts">SRE: Concepts and Principles</a> is your entry point into Site Reliability Engineering. Over 90 minutes, you’ll follow two experienced SREs as they deal with real production scenarios.</p>

<h3 id="what-youll-learn">What You’ll Learn</h3>

<p>The course covers the foundational SRE concepts through practical demonstrations:</p>

<ul>
  <li>How SRE differs from traditional IT operations and DevOps</li>
  <li>Service Level Indicators (SLIs), Service Level Objectives (SLOs), and error budgets</li>
  <li>Incident management and the importance of blameless postmortems</li>
  <li>Core SRE tools for monitoring and alerting</li>
  <li>Automation, automation, automation</li>
</ul>

<p><img src="/content/images/2025/07/sre-1-automate.png" alt="Automation is the key principle in SRE" /></p>

<h3 id="course-outline">Course Outline</h3>

<p><strong>Module 1: Investigating Issues: On-Call with an SRE</strong><br />
Follow an on-call SRE dealing with a disk space issue in <a href="https://www.elastic.co/elasticsearch/">Elasticsearch</a>. You’ll see how SREs approach problems differently from traditional ops teams, using engineering practices to solve operational challenges.</p>

<p><strong>Module 2: Classifying and Tracking Performance with Service Levels</strong><br />
Join another SRE investigating a performance issue that’s burning through error budget. This module explains the key concepts of SLIs and SLOs while demonstrating logging and distributed tracing tools.</p>

<p><strong>Module 3: Managing Risk and Reducing Downtime</strong><br />
Learn how to use monitoring tools like <a href="https://prometheus.io/">Prometheus</a> and <a href="https://grafana.com/">Grafana</a> with <a href="https://opentelemetry.io/">OpenTelemetry</a> to confirm root causes and work with development teams on architectural solutions.</p>

<p><strong>Module 4: Handling Failure with Incident Management</strong><br />
When the initial fix doesn’t work and the incident escalates, you’ll see how SREs use a structured incident management approach to investigate and get to quick resolution.</p>

<p><strong>Module 5: Reflecting and Improving Practices with Postmortems</strong><br />
Wrap up with a blameless postmortem that connects both incidents and provides a path forward for preventing future issues.</p>

<h2 id="course-2-sre-monitoring-and-observability">Course 2: SRE Monitoring and Observability</h2>

<p><a href="/l/ps-sre-monitoring">SRE: Monitoring and Observability</a> builds on the foundational knowledge from course 1. You’ll follow an SRE team preparing to onboard a new application into their production environment.</p>

<h3 id="what-youll-learn-1">What You’ll Learn</h3>

<p>This course focuses on the technical implementation of observability:</p>

<ul>
  <li>The three pillars of observability: logging, metrics, and tracing</li>
  <li>Setting up monitoring stacks with Elasticsearch, Prometheus, and Grafana</li>
  <li>Designing effective alerting strategies that avoid alert fatigue</li>
  <li>Automating incident response with <a href="https://www.redhat.com/en/topics/devops/what-is-ci-cd">CI/CD</a> pipelines</li>
  <li>Exploring <a href="https://www.gartner.com/en/information-technology/glossary/aiops-artificial-intelligence-operations">AIOps</a> and machine learning for advanced monitoring</li>
</ul>

<p><img src="/content/images/2025/07/sre-2-monitor.png" alt="Monitoring applications in SRE with OpenTelemetry" /></p>

<h3 id="course-outline-1">Course Outline</h3>

<p><strong>Module 1: Onboarding to SRE: Observability Requirements</strong><br />
Learn what data applications need to expose for SRE teams to manage them effectively. Covers structured logging with the <a href="https://www.elastic.co/what-is/elk-stack">EFK stack</a> and distributed tracing with <a href="https://grafana.com/oss/tempo/">Tempo</a>.</p>

<p><strong>Module 2: Measuring “Good” with Service Level Indicators</strong><br />
Deep dive into implementing meaningful SLIs using Prometheus, including how to expose metrics from application components and aggregate them for monitoring.</p>

<p><strong>Module 3: Alerting on “Bad” to Trigger Incident Response</strong><br />
Design alerting strategies that trigger the right response - automated fixes for known issues or pages for unknown problems. Includes integration with <a href="https://www.atlassian.com/software/opsgenie">OpsGenie</a>.</p>

<p><strong>Module 4: Automating Remediation with Pipelines</strong><br />
Reduce toil by automating common fixes using <a href="https://github.com/features/actions">GitHub Actions</a> workflows triggered by your monitoring stack, with status updates posted to <a href="https://slack.com/">Slack</a>.</p>

<p><strong>Module 5: Next-level SRE: Machine Learning and AIOps</strong><br />
Explore how AIOps platforms like <a href="https://www.datadoghq.com/">Datadog</a> can augment traditional SRE practices with machine learning-driven anomaly detection and automated incident analysis.</p>

<h2 id="real-world-tools-and-practices">Real-World Tools and Practices</h2>

<p>Both courses use the same tools you’ll find in production SRE environments:</p>

<ul>
  <li><strong>Monitoring</strong>: Prometheus, Grafana</li>
  <li><strong>Logging</strong>: Elasticsearch, <a href="https://www.elastic.co/kibana/">Kibana</a>, <a href="https://www.fluentd.org/">Fluentd</a></li>
  <li><strong>Tracing</strong>: Tempo, OpenTelemetry</li>
  <li><strong>Alerting</strong>: OpsGenie, <a href="https://www.pagerduty.com/">PagerDuty</a></li>
  <li><strong>AIOps</strong>: Datadog</li>
</ul>

<p>Every demo shows working implementations to back up the theory. You’ll see realistic incidents being investigated, actual dashboards being built, and automation workflows in action.</p>

<p><img src="/content/images/2025/07/sre-2-alert.png" alt="Alerting thresholds in SRE" /></p>

<h2 id="who-should-take-these-courses">Who Should Take These Courses?</h2>

<p>The courses are designed for:</p>

<ul>
  <li>Developers who work with SRE teams or want to understand SRE practices</li>
  <li>Operations engineers looking to transition to SRE</li>
  <li>Team leads and managers evaluating SRE for their organization</li>
  <li>Anyone involved in digital transformation initiatives</li>
</ul>

<p>No deep technical knowledge is required for the first course - just a basic understanding of software development and deployment processes.</p>

<h2 id="whats-next">What’s Next?</h2>

<p>The next two courses in the learning path will complete your SRE education:</p>

<p><strong>SRE: Resiliency and Automation</strong> will focus on building systems that can withstand failures and automating responses to common issues. You’ll learn how to design for resilience, implement chaos engineering practices, and create self-healing systems.</p>

<p><strong>SRE: Measuring and Optimizing Reliability</strong> will cover advanced techniques for quantifying and improving system reliability, including complex SLO hierarchies, reliability budgeting, and using data to drive architectural decisions.</p>

<h2 id="getting-started">Getting Started</h2>

<p>Ready to learn how Google keeps systems running at scale? Start your Site Reliability Engineering journey today:</p>

<ol>
  <li><strong><a href="/l/ps-sre-path">View the complete SRE learning path</a></strong> - See all 4 courses and plan your learning</li>
  <li><strong><a href="/l/ps-sre-concepts">Start with SRE: Concepts and Principles</a></strong> - Master the fundamentals (90 minutes)</li>
  <li><strong><a href="/l/ps-sre-monitoring">Continue with SRE: Monitoring and Observability</a></strong> - Implement real-world solutions</li>
</ol>

<p>Site Reliability Engineering isn’t just for Google-scale operations. These SRE principles and practices work for any team running production systems. Whether you’re managing a handful of microservices or hundreds, SRE provides a proven framework for balancing reliability with feature velocity and reducing operational toil.</p>

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>Q: Do I need prior SRE experience to take these courses?</strong><br />
A: No, the first course starts with fundamentals. Basic software development and deployment knowledge is helpful.</p>

<p><strong>Q: What tools will I learn?</strong><br />
A: Prometheus, Grafana, Elasticsearch, Kibana, Tempo, OpsGenie, and modern AIOps platforms like Datadog.</p>

<p><strong>Q: How long does the complete learning path take?</strong><br />
A: The four courses total approximately 6 hours, designed to be completed over 2-3 weeks.</p>

<p><strong>Q: Is this Google’s exact SRE approach?</strong><br />
A: These courses teach the core SRE principles Google pioneered, adapted for use in any size of organization.</p>

<p class="notice--info">Ready to dive deeper into the tools and practices that make SRE possible? Check out my other courses on <a href="/l/ps-home">Pluralsight</a> covering Docker, Kubernetes, and cloud-native architecture.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="sre" /><category term="site-reliability-engineering" /><category term="pluralsight" /><category term="monitoring" /><category term="observability" /><category term="devops" /><category term="google-sre" /><category term="incident-management" /><category term="prometheus" /><category term="grafana" /><summary type="html"><![CDATA[Master Site Reliability Engineering with my new 4-course Pluralsight learning path. Learn Google's SRE practices, monitoring with Prometheus & Grafana, incident management, and production observability through hands-on demonstrations.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.sixeyed.com/content/images/2025/07/sre-1-hero.png" /><media:content medium="image" url="https://blog.sixeyed.com/content/images/2025/07/sre-1-hero.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Locking Helm Releases to Prevent Upgrades (and Downgrades)</title><link href="https://blog.sixeyed.com/locking-helm-releases/" rel="alternate" type="text/html" title="Locking Helm Releases to Prevent Upgrades (and Downgrades)" /><published>2024-10-16T08:00:00+00:00</published><updated>2024-10-16T08:00:00+00:00</updated><id>https://blog.sixeyed.com/locking-helm-releases</id><content type="html" xml:base="https://blog.sixeyed.com/locking-helm-releases/"><![CDATA[<h2 id="the-challenge-preventing-unwanted-helm-upgrades-and-downgrades">The Challenge: Preventing Unwanted Helm Upgrades and Downgrades</h2>

<p>It’s great having a single ‘Up’ pipeline for your apps which deploys the whole stack, creating whatever resources it needs and ensuring the deployment matches the spec in your source repo. Idempotence is the key here so your IaC will create or update infrastructure as required, and if you’re using <a href="/getting-started-with-kubernetes-on-windows/">Kubernetes</a> and Helm then you get desired-state deployment for the software.</p>

<p>One small issue you might see is if you have common services - say a data storage or monitoring subsystem - which are shared for multiple deployments of the app. If those deployments are different test environments running from different branches of the code then you might get into a tricky scenario:</p>

<ul>
  <li>you update the shared service Helm chart to v1.1 in the dev branch</li>
  <li>you run the Up pipeline to deploy to the latest code to the dev environment</li>
  <li>later someone deploys an earlier version from a release branch to the test environment</li>
  <li>the release branch uses v1.0 of the Helm chart, so your shared service gets downgraded</li>
</ul>

<p>Helm has the <code class="language-plaintext highlighter-rouge">upgrade --install</code> command which supports this idempotent approach, but there’s no flag to say <em>install it if it hasn’t been deployed yet, or upgrade it if it has - but only upgrade it if this version number is higher than the one for the current release</em>. In that case it would be useful to lock the release to prevent any further upgrades or downgrades, but there’s no <code class="language-plaintext highlighter-rouge">helm lock</code> command either.</p>

<h2 id="pending-status-to-the-rescue">Pending Status to the Rescue</h2>

<p>When Helm installs and upgrades get interrupted they can leave the release in a pending state - <code class="language-plaintext highlighter-rouge">pending-upgrade</code> or <code class="language-plaintext highlighter-rouge">pending-rollback</code>, usually when an operation times out. It’s a nasty situation which requires manually deleting the Helm release Secret (until this <a href="https://github.com/helm/community/pull/354">HIP</a> is completed) - but it effectively prevents any further changes to the release, so we can abuse it to create a lock.</p>

<p>The scripting for this is fairly simple, but it does rely on the internals of how Helm represents a release, so it’s liable to be broken at some point (it’s working as of Helm 3.16). Every time you install or upgrade a release Helm creates a Kubernetes Secret which contains an encoded representation of the release.</p>

<p>You can try this with a simple Helm chart from my book <a href="https://amzn.to/3x3O7mt">Learn Kubernetes in a Month of Lunches</a> (see also my <a href="/learn-docker-in-a-month-of-lunches-my-new-book/">Docker book announcement</a>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add kiamol https://kiamol.net

helm repo update

helm -n default upgrade --install vweb kiamol/vweb
</code></pre></div></div>

<p>The <a href="https://github.com/sixeyed/kiamol/tree/master/ch10/vweb/v1/vweb">Helm chart</a> models a Deployment and a Service, but the install also creates a Secret:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PS&gt;kubectl get secret

NAME                         TYPE                 DATA   AGE
sh.helm.release.v1.vweb.v1   helm.sh/release.v1   1      3m18s
</code></pre></div></div>

<p>In the Secret is all the chart contents, plus metadata about the release.</p>

<h2 id="inspecting-the-helm-secret">Inspecting the Helm Secret</h2>

<p>You can decode the Secret but that won’t help you much - the content is in the <code class="language-plaintext highlighter-rouge">release</code> field, and it’s a ZIP file, encoded as a Base64 text stream. So to read the contents you need to decode the Base64 representation in Kubernetes, then <em>decode it again</em> to get the raw ZIP content, then pass it through the <code class="language-plaintext highlighter-rouge">gunzip</code> tool.</p>

<p>This extracts the raw data into a JSON file (using a *nix shell):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get secrets sh.helm.release.v1.vweb.v1 -o=jsonpath='{ .data.release }' | base64 -d | base64 -d | gunzip -c &gt; data_release.json
</code></pre></div></div>

<p>In the JSON you’ll see the YAML manifest for the deployment which Helm generated, plus the original chart contents. The interesting fields for us though are <code class="language-plaintext highlighter-rouge">info</code> and <code class="language-plaintext highlighter-rouge">version</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vweb"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"info"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"first_deployed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-10-16T07:53:28.496644+01:00"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"last_deployed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-10-16T07:53:28.496644+01:00"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"deleted"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
        </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Install complete"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"deployed"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>When you run a <code class="language-plaintext highlighter-rouge">helm upgrade</code> command it decodes all this and checks the value of <code class="language-plaintext highlighter-rouge">info.status</code> before it proceeds. If it sees the release is pending then it won’t continue.</p>

<h2 id="updating-the-helm-secret-to-lock-the-release">Updating the Helm Secret to Lock the Release</h2>

<p>Now we can see how to trick Helm into blocking any updates. The process is:</p>

<ul>
  <li>extract and decode and unzip the <code class="language-plaintext highlighter-rouge">release</code> value from the Secret into a JSON file</li>
  <li>update the <code class="language-plaintext highlighter-rouge">info.status</code> value in the JSON</li>
  <li>also increment the <code class="language-plaintext highlighter-rouge">version</code> field and set a useful description</li>
  <li>zip and encode the updated <code class="language-plaintext highlighter-rouge">release</code> JSON</li>
  <li>get the Secret and store as a YAML file</li>
  <li>update the <code class="language-plaintext highlighter-rouge">release</code> field in the YAML with the new data</li>
  <li>update the YAML metadata</li>
  <li>apply the updated Secret YAML</li>
</ul>

<p class="notice--info">I use <a href="https://mikefarah.gitbook.io/yq">yq</a> to make the JSON and YAML updates.</p>

<p>In Bash it looks like this - setting some variables first for the release we want to lock (fetch them from <code class="language-plaintext highlighter-rouge">helm ls</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">RELEASE_NAMESPACE</span><span class="o">=</span><span class="s2">"default"</span>
<span class="nv">RELEASE_NAME</span><span class="o">=</span><span class="s2">"vweb"</span>
<span class="nv">RELEASE_VERSION</span><span class="o">=</span><span class="s2">"1"</span>

<span class="nv">RELEASE_SECRET_NAME</span><span class="o">=</span><span class="s2">"sh.helm.release.v1.</span><span class="nv">$RELEASE_NAME</span><span class="s2">.v</span><span class="nv">$RELEASE_VERSION</span><span class="s2">"</span>

<span class="nb">echo</span> <span class="s2">"Fetching release JSON from secret: </span><span class="nv">$RELEASE_SECRET_NAME</span><span class="s2">"</span>
kubectl get secrets <span class="nt">-n</span> <span class="nv">$RELEASE_NAMESPACE</span> <span class="nv">$RELEASE_SECRET_NAME</span> <span class="nt">-o</span><span class="o">=</span><span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{ .data.release }'</span> | <span class="nb">base64</span> <span class="nt">-d</span> | <span class="nb">base64</span> <span class="nt">-d</span> | <span class="nb">gunzip</span> <span class="nt">-c</span> <span class="o">&gt;</span> data_release.json

<span class="nb">let</span> <span class="s2">"NEW_VERSION=RELEASE_VERSION+1"</span>
<span class="nb">echo</span> <span class="s2">"Updating release JSON with lock data and new version: </span><span class="nv">$NEW_VERSION</span><span class="s2">"</span>
<span class="nv">v</span><span class="o">=</span><span class="nv">$NEW_VERSION</span> yq <span class="nt">-i</span> <span class="s1">'.version = env(v)'</span> data_release.json
yq <span class="nt">-i</span> <span class="s1">'.info.status = "pending-upgrade"'</span> data_release.json
yq <span class="nt">-i</span> <span class="s1">'.info.description = "LOCKED"'</span> data_release.json

<span class="nb">echo</span> <span class="s2">"Fetching release secret YAML"</span>
kubectl get secrets <span class="nt">-n</span> <span class="nv">$RELEASE_NAMESPACE</span> <span class="nv">$RELEASE_SECRET_NAME</span> <span class="nt">-o</span><span class="o">=</span>yaml <span class="o">&gt;</span> release_secret.yaml

<span class="nv">NEW_SECRET_NAME</span><span class="o">=</span><span class="s2">"sh.helm.release.v1.</span><span class="nv">$RELEASE_NAME</span><span class="s2">.v</span><span class="nv">$NEW_VERSION</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"Updating secret YAML with lock JSON and new name: </span><span class="nv">$NEW_SECRET_NAME</span><span class="s2">"</span>
yq <span class="nt">-i</span> <span class="s1">'del(.data)'</span> release_secret.yaml
yq <span class="nt">-i</span> <span class="s1">'del(.metadata.creationTimestamp)'</span> release_secret.yaml
yq <span class="nt">-i</span> <span class="s1">'del(.metadata.resourceVersion)'</span> release_secret.yaml
yq <span class="nt">-i</span> <span class="s1">'del(.metadata.uid)'</span> release_secret.yaml
<span class="nv">r</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat </span>data_release.json | <span class="nb">gzip</span> <span class="nt">-c</span> | <span class="nb">base64</span> <span class="nt">-w0</span><span class="si">)</span> yq <span class="nt">-i</span> <span class="s1">'.stringData.release = env(r)'</span> release_secret.yaml
<span class="nv">v</span><span class="o">=</span><span class="nv">$NEW_VERSION</span> yq <span class="nt">-i</span> <span class="s1">'.metadata.labels.version = strenv(v)'</span> release_secret.yaml
yq <span class="nt">-i</span> <span class="s1">'.metadata.labels.status = "pending-upgrade"'</span> release_secret.yaml
yq <span class="nt">-i</span> <span class="s1">'.metadata.labels.locked = "true"'</span> release_secret.yaml
<span class="nv">n</span><span class="o">=</span><span class="nv">$NEW_SECRET_NAME</span> yq <span class="nt">-i</span> <span class="s1">'.metadata.name = env(n)'</span> release_secret.yaml

kubectl apply <span class="nt">-f</span> release_secret.yaml
</code></pre></div></div>

<p>When you run this it creates a new Kubernetes Secret with the chart contents from the previous release, but with the status set to <code class="language-plaintext highlighter-rouge">pending-upgrade</code>, which is what locks the release. It also adds a label to the Secret - <code class="language-plaintext highlighter-rouge">locked=true</code> - which makes it easy to undo the lock later.</p>

<h2 id="locking-and-unlocking-the-helm-release">Locking and Unlocking the Helm Release</h2>

<p>If you try this out it should end with the happy message <code class="language-plaintext highlighter-rouge">secret/sh.helm.release.v1.vweb.v2 created</code>. Check your Helm releases and you’ll see the <code class="language-plaintext highlighter-rouge">vweb</code> app is now at revision 2 and is in <code class="language-plaintext highlighter-rouge">pending-upgrade</code> status:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span>helm <span class="nb">ls</span> <span class="nt">--all</span>
NAME    NAMESPACE       REVISION        UPDATED                              STATUS           CHART           APP VERSION
vweb    default         2               2024-10-16 07:53:28.496644 +0100 BST pending-upgrade  vweb-2.0.0      2.0.0
</code></pre></div></div>

<p>Adding the new Secret mimics a <code class="language-plaintext highlighter-rouge">helm upgrade</code> command which timed out and left the release pending. You can see the new Secret has the <code class="language-plaintext highlighter-rouge">status</code> label and also the <code class="language-plaintext highlighter-rouge">locked</code> label:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span>kubectl get secret <span class="nt">--show-labels</span>
NAME                         TYPE                 DATA   AGE     LABELS
sh.helm.release.v1.vweb.v1   helm.sh/release.v1   1      29m     <span class="nv">name</span><span class="o">=</span>vweb,owner<span class="o">=</span>helm,status<span class="o">=</span>deployed,version<span class="o">=</span>1
sh.helm.release.v1.vweb.v2   helm.sh/release.v1   1      2m56s   <span class="nv">locked</span><span class="o">=</span><span class="nb">true</span>,name<span class="o">=</span>vweb,owner<span class="o">=</span>helm,status<span class="o">=</span>pending-upgrade,version<span class="o">=</span>2
</code></pre></div></div>

<blockquote>
  <p>The status label is just a convenience - updating that on its own doesn’t lock the release, you need to update the status field in the release JSON</p>
</blockquote>

<p>Any attempt to run a <code class="language-plaintext highlighter-rouge">helm upgrade</code> will fail now:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span>helm upgrade <span class="nt">--install</span> vweb kiamol/vweb
Error: UPGRADE FAILED: another operation <span class="o">(</span><span class="nb">install</span>/upgrade/rollback<span class="o">)</span> is <span class="k">in </span>progress
</code></pre></div></div>

<p>You can unlock the release by deleting the Secret:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete secret <span class="nt">-l</span> <span class="nv">owner</span><span class="o">=</span>helm,locked<span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p>And now you can merrily upgrade again:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span>helm upgrade <span class="nt">--install</span> vweb kiamol/vweb        
Release <span class="s2">"vweb"</span> has been upgraded. Happy Helming!
NAME: vweb
LAST DEPLOYED: Wed Oct 16 08:26:49 2024
NAMESPACE: tracing-sample
STATUS: deployed
REVISION: 2
TEST SUITE: None
</code></pre></div></div>

<p>All that’s left is to tidy up the Bash script and wrap it into a Docker image with <code class="language-plaintext highlighter-rouge">bash</code>, <code class="language-plaintext highlighter-rouge">kubectl</code> and <code class="language-plaintext highlighter-rouge">yq</code> installed so you can run it without needing all the dependencies…</p>

<h2 id="related-reading">Related Reading</h2>

<p>If you’re working with Kubernetes and containers, you might find these related posts helpful:</p>

<ul>
  <li><a href="/getting-started-with-kubernetes-on-windows/">Getting Started with Kubernetes on Windows</a> - A comprehensive introduction to setting up Kubernetes on Windows</li>
  <li><a href="/this-blog-runs-on-docker-and-kubernetes-in-azure/">This Blog Runs on Docker and Kubernetes in Azure</a> - Real-world example of running production workloads on Kubernetes</li>
  <li><a href="/you-cant-always-have-kubernetes-running-containers-in-azure-vm-scale-sets/">You Can’t Always Have Kubernetes: Running Containers in Azure VM Scale Sets</a> - Alternative approaches when Kubernetes isn’t suitable</li>
</ul>

<p>For more container orchestration insights, check out my <a href="/learn-docker-in-a-month-of-lunches-my-new-book/">Docker and Kubernetes learning resources</a>.</p>]]></content><author><name>Elton Stoneman</name><uri>/l/ps-home</uri></author><category term="helm" /><category term="kubernetes" /><category term="devops" /><category term="helm-charts" /><summary type="html"><![CDATA[Learn how to lock Helm releases in Kubernetes to prevent unwanted upgrades and downgrades. This step-by-step guide shows you how to manipulate Helm secrets to create release locks, protecting your shared services from version conflicts across multiple environments.]]></summary></entry></feed>