<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Western Wilson</title><link>https://westernwilson.com/tags/devops/</link><description>Recent content in Devops on Western Wilson</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://westernwilson.com/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>AGNTCon + MCPCon Europe 2026</title><link>https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/</guid><description>TL;DR: I spent the full second day at AGNTCon + MCPCon Europe in Amsterdam, and the most interesting talks showed a new normal for humans now that agents write most of the code.&#10;Dexter Horthy was the best speaker of the day, and Dylan Ratcliffe won me over after I initially disagreed with his premise. His line “easier to reject the recipe than the meal” is the same lesson I learnt the hard way at the bunq hackathon: i.e review the plan before anything gets built. Roy Belio’s red teaming lab made me glad I’d already locked down egress on my coding agent, and gave me a list of things I still haven’t done (DNS, canary tokens, read-only config). Another thing is that plenty of the live demos broke, which shows non-determinism in action.&#10;</description><content:encoded><![CDATA[<p><strong>TL;DR:</strong> I spent the full second day at <a href="https://events.linuxfoundation.org/agntcon-mcpcon-europe">AGNTCon + MCPCon Europe</a> in Amsterdam, and the most interesting talks showed a new normal for humans now that agents write most of the code.</p>
<p>Dexter Horthy was the best speaker of the day, and Dylan Ratcliffe won me over after I initially disagreed with his premise. His line &ldquo;easier to reject the recipe than the meal&rdquo; is the same lesson I learnt the hard way at the <a href="https://westernwilson.com/reflections/bunq-hackathon-7">bunq hackathon</a>: i.e review the plan before anything gets built. Roy Belio&rsquo;s red teaming lab made me glad I&rsquo;d already <a href="https://westernwilson.com/reflections/containerising-my-coding-agent">locked down egress on my coding agent</a>, and gave me a list of things I still haven&rsquo;t done (DNS, canary tokens, read-only config). Another thing is that plenty of the live demos broke, which shows non-determinism in action.</p>
<p>Next year&rsquo;s conference is in London, and I&rsquo;m hoping to go! Reach out if you&rsquo;re going too.</p>
<p>The rest of this post is my notes from each talk, lightly tidied up.</p>
<h2 id="catch-them-early" class="group relative scroll-mt-20">
  Catch Them Early
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#catch-them-early"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBEf/keynote-catch-them-early-manik-surtani-cto-agentic-ai-foundation"><strong>9:07am, Manik Surtani (CTO, Agentic AI Foundation)</strong></a></p>
<ul>
<li>The <a href="https://aaif.io/">Agentic AI Foundation (AAIF)</a> compared itself to the CNCF, which had the benefit of an industry that already had lots of experience with cloud.</li>
<li>They&rsquo;ve copied the CNCF bar for accepting projects, and in some places raised it.</li>
<li>They&rsquo;ve had a 3-4x growth pace compared to CNCF&hellip; the AI hype probably helped tbh</li>
</ul>
<h2 id="getting-to-stateless-mcp-in-production" class="group relative scroll-mt-20">
  Getting to stateless MCP in production
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#getting-to-stateless-mcp-in-production"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBHz/keynote-getting-to-stateless-mcp-in-production-shaun-smith-mcp-and-transport-working-group-maintainer-hugging-face"><strong>9:19am, Shaun Smith (Hugging Face, MCP transport working group maintainer)</strong></a></p>
<ul>
<li>Streamable HTTP is a smarter way to connect, closer to one-to-one client-to-server.</li>
<li>The stateless release brought improvements, but they needed migrating to the new SDK.</li>
<li>Uptake has been massive.</li>
</ul>
<h2 id="three-doors-to-one-tool-mcp-vs-webmcp-vs-cli" class="group relative scroll-mt-20">
  Three doors to one tool: MCP vs WebMCP vs CLI
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#three-doors-to-one-tool-mcp-vs-webmcp-vs-cli"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBVv/keynote-three-doors-to-one-tool-mcp-vs-webmcp-vs-cli-frederic-barthelet-cto-co-founder-alpic-dominic-farolino-editor-of-the-webmcp-specification-software-engineer-google"><strong>9:29am, Dominic Farolino (Google Chrome) &amp; Frederic Barthelet (CTO, Alpic)</strong></a></p>
<ul>
<li>WebMCP is a layer on a website that lets agents interact with your website much faster.</li>
<li>Better than <code>llms.txt</code> which isn&rsquo;t often looked at</li>
<li>CLIs integrate well with agents.</li>
<li>A harness is tools, memory, context, and guards.</li>
<li>MCP can be a local server or a public one.</li>
</ul>
<h2 id="state-of-the-software-factory" class="group relative scroll-mt-20">
  State of the software factory
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#state-of-the-software-factory"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBUc/keynote-state-of-the-software-factory-dexter-horthy-ceo-and-co-founder-humanlayer"><strong>9:41am, Dexter Horthy (CEO &amp; co-founder, HumanLayer)</strong></a></p>
<ul>
<li>A fantastic speaker who also has a YouTube channel.</li>
<li>The best talk of the day, with interesting content that&rsquo;s worth checking out properly.</li>
<li>Walked through HumanLayer&rsquo;s multiplayer AI chat tooling, which drives spec and code diff checks.</li>
</ul>
<p>Tools mentioned: <a href="https://hlyr.dev/resources">HumanLayer&rsquo;s resources</a>.</p>
<h2 id="pull-requests-are-dead-long-live-peer-review" class="group relative scroll-mt-20">
  Pull Requests are dead, long live peer review
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#pull-requests-are-dead-long-live-peer-review"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RB8Y/pull-requests-are-dead-long-live-peer-review-dylan-ratcliffe-overmind"><strong>10:20am, Dylan Ratcliffe (Overmind)</strong></a></p>
<p>The most controversial talk of the day, at first. By the end he&rsquo;d convinced me, mostly because he showed how it&rsquo;s working for them right now.</p>
<ul>
<li>Pull requests are dead.</li>
<li>AI is not your peer, and its code is harder to review.</li>
<li>Agents are agreeable. They&rsquo;re bad at disagreeing with you.</li>
<li>He was a passable engineer who was outperforming his own team, and considered firing the engineering team. Instead he shifted the engineers left, into more product-oriented roles.</li>
<li>Move peer review into the planning phase, where the decisions actually get made.</li>
<li>The loop: research, decide on an approach, weigh up tradeoffs, plan, collaborate, then implement until the implementation matches the plan.</li>
<li>It&rsquo;s easier to reject the recipe than the meal. People are more willing to reject a plan, because it&rsquo;s cheaper to fix.</li>
<li>My open question: what about conflicts between the plan and the existing system? Does that get caught in review?</li>
</ul>
<p>Tools mentioned: <a href="https://www.notyourpeer.com/">AI Is Not Your Peer</a>, and <a href="https://until.dev/">Until</a>, which makes sure your agent implements every part of the plans you write.</p>
<h2 id="from-works-on-my-prompt-to-production-slos" class="group relative scroll-mt-20">
  From &ldquo;works on my prompt&rdquo; to production SLOs
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#from-works-on-my-prompt-to-production-slos"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBA3/from-works-on-my-prompt-to-production-slos-building-agent-observability-manik-khandelwal-microsoft"><strong>10:55am, Manik Khandelwal (Microsoft)</strong></a></p>
<ul>
<li>He wants agents that don&rsquo;t need constant checking.</li>
<li>It was confusing what he was trying to do. He has a framework that watches whether an agent needs active supervision or can be left running in the background.</li>
<li>The slides were AI generated. Cool to look at and interactive, but they lacked humanity in a way I can&rsquo;t quite describe. It didn&rsquo;t tell a story, it was more show and tell.</li>
<li>I didn&rsquo;t get much from this one.</li>
</ul>
<p>Tools mentioned: Mitra, an internal tool for monitoring agents. Not sure if it&rsquo;s open sourced yet.</p>
<h2 id="governance-you-can-run-checkable-properties-for-production-agents" class="group relative scroll-mt-20">
  Governance you can run: Checkable properties for production agents
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#governance-you-can-run-checkable-properties-for-production-agents"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBAC/governance-you-can-run-checkable-properties-for-production-agents-seshu-tolety-siemens"><strong>11:30am, Seshu Tolety (Director of Agentic AI, Siemens)</strong></a></p>
<ul>
<li>The cost of ungoverned AI model usage on privacy, operational burden, and more.</li>
<li>Three lifecycles as new mental models: AI adoption, engineering, and operations (can we run AI safely at scale?).</li>
<li>He walked through today&rsquo;s organisation and policy structures, then out to a future state where AI governs AI policy.</li>
<li>Humans sit in that structure reviewing the policy that AI creates. As we know from code, that makes humans the bottleneck. After hearing Dylan&rsquo;s talk, I&rsquo;m not sure I agree. The idea is both scary and interesting at the same time!</li>
<li>&ldquo;Governance should never slow innovation. It should help you scale it.&rdquo;</li>
</ul>
<h2 id="governed-agent-autonomy-building-a-control-plane-for-agentic-systems" class="group relative scroll-mt-20">
  Governed agent autonomy: Building a control plane for agentic systems
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#governed-agent-autonomy-building-a-control-plane-for-agentic-systems"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBAR/governed-agent-autonomy-building-a-control-plane-for-agentic-systems-nnenna-ndukwe-qodo-ai"><strong>1:15pm, Nnenna Ndukwe (Qodo AI)</strong></a></p>
<ul>
<li>Harness config, mostly around what to allow.</li>
<li>Five governance patterns found in the leaked Claude Code source, which she open sourced:
<ol>
<li>Planning</li>
<li>Permissions (can this action proceed?)</li>
<li>Tool trust (capability trust)</li>
<li>Verification</li>
<li>Runtime (what were the outcomes?)</li>
</ol>
</li>
<li>She implemented them in a Rust agent.</li>
<li>The demo was supposed to block a deployment to production. It didn&rsquo;t xD</li>
</ul>
<p>Tools mentioned: Goose (an AAIF project), GAAP (agent observability), and Martin Fowler&rsquo;s writing on harness engineering.</p>
<h2 id="beyond-chatbots-agentic-ui-with-open-standards" class="group relative scroll-mt-20">
  Beyond Chatbots: Agentic UI with Open Standards
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#beyond-chatbots-agentic-ui-with-open-standards"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBAs/beyond-chatbots-agentic-ui-with-open-standards-manfred-steyer-angulararchitects"><strong>1:25pm, Manfred Steyer (ANGULARarchitects)</strong></a></p>
<ul>
<li>An interesting talk, and one of the few live demos that worked. Well, it didn&rsquo;t at first, until they realised it was just the WiFi. Once that was fixed it worked.</li>
<li>&ldquo;To chat or not to chat&hellip; that&rsquo;s the wrong question.&rdquo;</li>
<li>Showed a UI progressively changing as you chat with it and co-create.</li>
</ul>
<p>Tools mentioned: AG-UI (agent communication), A2UI (dynamic UI), and MCP Apps.</p>
<h2 id="mcp-apps-and-the-agentic-web" class="group relative scroll-mt-20">
  MCP Apps and the Agentic Web
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#mcp-apps-and-the-agentic-web"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBAU/mcp-apps-and-the-agentic-web-liad-yosef-mcp-apps"><strong>1:50pm, Liad Yosef (MCP Apps)</strong></a></p>
<ul>
<li>The difference between a website, WebMCP, and MCP Apps.</li>
<li>A really convincing talk on how brands can bring their apps into chat instead of splicing up their current website.</li>
<li>His point is that we might move away from browsers entirely, because we might not need them.</li>
<li>Salesforce is going headless, even though UI was their differentiator.</li>
<li>Cloudflare says bot traffic has overtaken human traffic.</li>
<li>Web search isn&rsquo;t great for agents, and an app store probably isn&rsquo;t either. What about an agentic identity that&rsquo;s tied to you?</li>
<li>The next big thing is the discovery layer for agents. He compared it to web search, with <code>.well-known/ai-catalog.json</code> as a unified standard.</li>
</ul>
<p>Tools mentioned: Goose, DoorDash&rsquo;s new CLI, Sentry, Ora&rsquo;s researcher benchmark, <code>npx ax</code> (an audit tool for how agents use your website), and Ora&rsquo;s <a href="https://ora.directory/">Agentic Resources Discovery: the agentic index of the web</a>.</p>
<h2 id="no-central-brain" class="group relative scroll-mt-20">
  No Central Brain
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#no-central-brain"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBBV/no-central-brain-fausto-albers-wonderwhy"><strong>3:25pm, Fausto Albers (WonderWhy, co-founder of AI Builders Club)</strong></a></p>
<ul>
<li>No central brain. Ominous music. Who decides what&rsquo;s true?</li>
<li>&ldquo;Attention is all you need&rdquo;, reframed as: 1. harness and loop, 2. recursive self improvement, 3. memory and learning.</li>
<li>The harness reinvents itself. It self-learns and stores each change along with its cost, tests itself, makes changes, evaluates the model, then recreates the harness.</li>
<li>I wonder if it&rsquo;ll converge on AI granting itself unlimited access and some wild harness configurations…</li>
</ul>
<h2 id="infrastructure-red-teaming-with-abliterated-models" class="group relative scroll-mt-20">
  Infrastructure red teaming with abliterated models
  <a
    href="https://westernwilson.com/reflections/agntcon-mcpcon-europe-2026/#infrastructure-red-teaming-with-abliterated-models"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><a href="https://agntconmcpconeu26.sched.com/event/2RBBJ/infrastructure-red-teaming-with-abliterated-models-what-actually-stops-agent-attacks-roy-belio-red-hat"><strong>3:30pm, Roy Belio (Red Hat)</strong></a></p>
<ul>
<li>Red teaming chat models and model refusal. Injection gets past a lot of guardrails.</li>
<li>There are lots of CVEs about prompt injection and ways around guardrails.</li>
<li>Guardrails and classifiers are a good start, but they&rsquo;ll miss anything that isn&rsquo;t in English.</li>
<li>His outcome was to run tooling on an SSH server somewhere else, not on the same service as the LLM.</li>
<li>In the interactive lab, a tool would sometimes error out, but <code>cat</code> was still available so the agent read the file anyway. Sometimes the LLM hid the logs too.</li>
<li>A read-only container running <code>sshd</code> is a good idea.</li>
<li>Watch for direct socket operations and exfiltration over DNS. Block egress, and keep DNS traffic internal too.</li>
<li>The slash command illusion.</li>
<li>Keep the config that shapes the agent&rsquo;s behaviour on the pods, and never let it be writable. The agent will rewrite itself.</li>
<li>Put canary tokens anywhere that could tell you you&rsquo;ve been compromised.</li>
</ul>
<p>Further reading he mentioned: EchoLeak (Microsoft), AWS Strands, MCP Poison (Cursor), the Deadbugz campaign (August 2026), and <em>Crash test dummy</em>, the blog where he first worked through this talk.</p>
]]></content:encoded></item><item><title>Containerising my coding agent</title><link>https://westernwilson.com/reflections/containerising-my-coding-agent/</link><pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/containerising-my-coding-agent/</guid><description>I containerised Claude Code this week because I wanted to let it run with fewer guardrails and not hand it unlimited access the rest of my machine. There’s a Hacker News thread from 6 months ago about someone running Claude in yolo mode who asked it to clean something up and watched it rm -rf their home directory 😅. So let’s aim to at least avoid that.&#10;Thankfully, I found Dev Containers, which turned into its own rabbit hole but lets us lock down Claude without having to maintain our own docker image.&#10;</description><content:encoded><![CDATA[<p>I containerised Claude Code this week because I wanted to let it run with fewer guardrails and not hand it unlimited access the rest of my machine. There&rsquo;s a <a href="https://news.ycombinator.com/item?id=46268222">Hacker News thread</a> from 6 months ago about someone running Claude in yolo mode who asked it to clean something up and watched it <code>rm -rf</code> their home directory 😅. So let&rsquo;s aim to at least avoid that.</p>
<p>Thankfully, I found <a href="https://containers.dev">Dev Containers</a>, which turned into its own rabbit hole but lets us lock down Claude without having to maintain our own docker image.</p>
<p>This reflection post talks through the containerised setup and some issues I faced while setting it up. If you just want the solution, then the implementation is in the <a href="https://github.com/wjkw1/westernwilson.com/tree/main/.devcontainer">.devcontainer/</a> directory of this website&rsquo;s source code.</p>
<h2 id="feature-or-docker-image" class="group relative scroll-mt-20">
  Feature or docker image?
  <a
    href="https://westernwilson.com/reflections/containerising-my-coding-agent/#feature-or-docker-image"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>I assumed that Anthropic would ship an official Claude base image you build on top of. They don&rsquo;t, they only post a recommended starting point. Claude Code installs into any <a href="https://code.claude.com/docs/en/devcontainer">Dev Container</a> through the <code>ghcr.io/anthropics/devcontainer-features/claude-code:1.0</code> feature, which you reference in <code>devcontainer.json</code> like <a href="https://containers.dev/features">any other devcontainers feature</a>. It works wherever the Dev Containers spec is supported e.g. VS Code, Codespaces, JetBrains, and (with some rough edges) Cursor. In VS Code it can be configured to pull in the Claude Code extension, keeping Claude decoupled from the base image entirely.</p>
<p>That leaves three layers to assemble: a base image, the tooling you use, and Claude Code on top. The real decision is whether you bake those middle two layers into a custom docker image you push to a registry, or assemble them per-repo through <code>devcontainer.json</code> features. A custom image is the better call for a team, where you want a validated, secure, and reusable environment everyone builds on. For a solo setup spun up per-repo, features are easier. The cost is a slow first build and less control over exactly what lands in the image, which is what <code>post-create.sh</code> helps automate somewhat.</p>
<p>My <code>devcontainer.json</code> ended up looking like this, trimmed to the main parts:</p>
<div class="highlight"><pre tabindex="0"><code class="language-jsonc" data-lang="jsonc"><span><span>{
</span></span><span><span>  <span>&#34;image&#34;</span>: <span>&#34;mcr.microsoft.com/devcontainers/base:ubuntu&#34;</span>,
</span></span><span><span>  <span>&#34;features&#34;</span>: {
</span></span><span><span>    <span>&#34;ghcr.io/anthropics/devcontainer-features/claude-code:1.0&#34;</span>: {}
</span></span><span><span>  },
</span></span><span><span>  <span>&#34;remoteUser&#34;</span>: <span>&#34;vscode&#34;</span>,
</span></span><span><span>  <span>&#34;mounts&#34;</span>: [
</span></span><span><span>    <span>&#34;source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume&#34;</span>
</span></span><span><span>  ],
</span></span><span><span>  <span>&#34;runArgs&#34;</span>: [<span>&#34;--cap-add=NET_ADMIN&#34;</span>, <span>&#34;--cap-add=NET_RAW&#34;</span>],
</span></span><span><span>  <span>&#34;postCreateCommand&#34;</span>: <span>&#34;sudo zsh .devcontainer/post-create.sh&#34;</span>,
</span></span><span><span>  <span>&#34;postStartCommand&#34;</span>: <span>&#34;sudo /usr/local/sbin/init-firewall.sh&#34;</span>,
</span></span><span><span>  <span>&#34;waitFor&#34;</span>: <span>&#34;postStartCommand&#34;</span>
</span></span><span><span>}
</span></span></code></pre></div><p>The non-root <code>vscode</code> user is important if you ever run with <code>--dangerously-skip-permissions</code>, Claude Code requires it. The named volume on <code>~/.claude</code> means login persists across rebuilds instead of re-authenticating every time. The <code>NET_ADMIN</code>/<code>NET_RAW</code> capabilities exist so that we can create the containerised firewall. <code>waitFor: postStartCommand</code> blocks the terminal until that firewall script finishes, so there&rsquo;s no window where Claude is already running before egress is locked down.</p>
<p><code>post-create.sh</code> is the everyday stuff a feature-based setup doesn&rsquo;t hand you for free: setting git aliases, adding a couple of zsh aliases for <code>kubectl</code> and <code>terraform</code>, locking down <code>sudo</code>/<code>su</code>.</p>
<p><code>init-firewall.sh</code> sets default-DROP policies on iptables, then builds an <code>ipset</code> allowlist of everything the container is allowed to reach outbound. Part of that allowlist is GitHub&rsquo;s own published IP ranges, fetched live at container start:</p>
<div class="highlight"><pre tabindex="0"><code class="language-zsh" data-lang="zsh"><span><span>gh_ranges<span>=</span><span>$(</span>curl -s https://api.github.com/meta<span>)</span>
</span></span><span><span><span>while</span> read -r cidr; <span>do</span>
</span></span><span><span>    ipset add allowed-domains <span>&#34;</span>$cidr<span>&#34;</span>
</span></span><span><span><span>done</span> &lt; &lt;<span>(</span>echo <span>&#34;</span>$gh_ranges<span>&#34;</span> | jq -r <span>&#39;(.web + .api + .git)[]&#39;</span> | aggregate -q<span>)</span>
</span></span></code></pre></div><p>Everything else —npm, <code>api.anthropic.com</code>, the VS Code marketplace— gets resolved and added individually. The firewall script finishes by checking its own work: it tries <code>curl https://example.com</code> and expects that to fail, then tries <code>curl https://api.github.com/zen</code> and expects that to succeed.</p>
<h2 id="firewall-and-sudo" class="group relative scroll-mt-20">
  Firewall and sudo
  <a
    href="https://westernwilson.com/reflections/containerising-my-coding-agent/#firewall-and-sudo"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>I spent a decent chunk of time configuring who&rsquo;s allowed to touch <code>iptables</code>. The <code>common-utils</code> feature grants the <code>vscode</code> user blanket passwordless <code>sudo</code> by default, and my first cut of <code>post-create.sh</code> narrowed that down to the <code>iptables</code>/<code>iptables-save</code>/<code>ipset</code> binaries themselves, with no argument restriction. Which meant Claude or anything else running as <code>vscode</code>, including under <code>--dangerously-skip-permissions</code>, could just run <code>sudo iptables -F &amp;&amp; sudo iptables -P OUTPUT ACCEPT</code> and the firewall would be gone! Anthropic&rsquo;s own reference devcontainer avoids this by granting <code>sudo</code> on one fixed script path instead of the raw binaries, baked into their image at build time so the script itself can&rsquo;t be edited either. I didn&rsquo;t want a custom Dockerfile yet, but dev containers can do the same thing with just <code>postCreateCommand</code>: copy <code>init-firewall.sh</code> to a root-owned <code>/usr/local/sbin/init-firewall.sh</code> outside the bind-mounted workspace while the feature&rsquo;s broad sudo grant is still active, then narrow <code>sudo</code> down to NOPASSWD on that one frozen executable file only. The tradeoff is that editing the live <code>init-firewall.sh</code> to allow a new domain now needs a full <strong>Rebuild Container</strong>, not just a restart, since the frozen copy is what actually runs.</p>
<p>Claude has it&rsquo;s own way to limit it&rsquo;s own execution too using <code>.claude/settings.json</code>. We&rsquo;ve configured this on top to deny any <code>Bash(sudo *)</code> and <code>Bash(su *)</code> commands, so in normal mode Claude won&rsquo;t even attempt the command. But <code>--dangerously-skip-permissions</code> does exactly what its name says: it bypasses every permission rule, deny rules included. So that file is a speed bump for the default mode, not something to lean on once the safety&rsquo;s off. The frozen-script sudoers restriction is the one that locks it down regardless, because it&rsquo;s enforced by the OS, not by Claude Code.</p>
<h2 id="gap-in-the-firewall" class="group relative scroll-mt-20">
  Gap in the firewall
  <a
    href="https://westernwilson.com/reflections/containerising-my-coding-agent/#gap-in-the-firewall"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>One thing I noticed was that <code>api.github.com/meta</code>&rsquo;s ip ranges aren&rsquo;t scoped to the API. They cover GitHub&rsquo;s whole edge network, which includes GitHub Pages in some cases. For example, this website, <a href="https://westernwilson.com">westernwilson.com</a>, runs on GitHub Pages. So with the firewall up and supposedly restricting the agent to an approved set of domains, I could still reach my own site. And by the same logic, any other <code>*.github.io</code> page, or anything hosted there by someone with worse intentions. A page on <code>*.github.io</code> could potentially hold a payload hosted somewhere else entirely, and the firewall would wave it through without ever knowing the difference.</p>
<p>I see three ways to close that off:</p>
<ul>
<li>Proxy the traffic and filter on hostname instead of IP. Cleanest in theory, but Encrypted Client Hello can still hide the hostname from anything sitting.</li>
<li>Drop <code>*.github.io</code> from the allowlist entirely. Unfortunately this also takes <code>raw.githubusercontent.com</code> out too since they share the same ip ranges, but if that&rsquo;s not something the container needs, it&rsquo;s a fine trade.</li>
<li>Accept the risk and move on.</li>
</ul>
<p>I haven&rsquo;t picked one yet, for now I&rsquo;m sitting with the risk while I think about which tradeoff is actually worth it.</p>
<h2 id="logging-in-to-claude" class="group relative scroll-mt-20">
  Logging in to Claude
  <a
    href="https://westernwilson.com/reflections/containerising-my-coding-agent/#logging-in-to-claude"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Logging in had its own gotcha. In VS Code there is a big orange &ldquo;use your subscription&rdquo; button that opens a browser and waits for a callback to <code>localhost</code>, which never makes it back into the container and just hangs. The fix is to skip the button, open a terminal inside the container and run <code>claude</code>, which kicks off the same browser flow but resolves the callback correctly.</p>
<p>We then use <code>~/.claude</code>, which is now owned by the <code>vscode</code> thanks to the <code>chown</code> at the top of <code>post-create.sh</code>, so once logged in, we gain stickier login sessions.</p>
<p>In doing the whole login dance from a bare terminal meant that my first real session with Claude Code inside the dev container was just text based interaction with no IDE extension wrapper around it at all. It&rsquo;s an interesting way to work and removes a lot of the UI distraction, but not something I&rsquo;ll rely on fulltime.</p>
<h2 id="port-forwarding" class="group relative scroll-mt-20">
  Port forwarding
  <a
    href="https://westernwilson.com/reflections/containerising-my-coding-agent/#port-forwarding"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>The other thing that briefly worried me is VS Code forwarded around thirty-two ports the moment the container came up. My first thought was that something in the container was reaching out on its own. It&rsquo;s not, after some searching I found that it&rsquo;s VS Code&rsquo;s automatic port-forwarding scanning the container for anything listening on a port, for example a dev server that you&rsquo;ll want to open in a browser. It&rsquo;s a separate, fairly trigger-happy feature, and apparently a known source of noise; <a href="https://github.com/microsoft/vscode-remote-release/issues/10926">This GitHub issue</a> describes the exact same &ldquo;over 20 ports auto-forwarded&rdquo; behaviour after a setting silently flips from <code>process</code> to <code>hybrid</code> detection mode.</p>
<p>If you want to see the actual config rather than the excerpts above, it&rsquo;s all in this repo under <a href="https://github.com/wjkw1/westernwilson.com/tree/main/.devcontainer">.devcontainer/</a>. Treat it as a personal setup to extend, not a hardened image to copy verbatim.</p>
<p>Further reading: Anthropic&rsquo;s <a href="https://code.claude.com/docs/en/devcontainer">Dev Containers docs</a> and the <a href="https://code.visualstudio.com/docs/devcontainers/tutorial">VS Code Dev Containers tutorial</a>.</p>
]]></content:encoded></item><item><title>DevOps vs Platform vs Infrastructure Engineer</title><link>https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/</guid><description>I’ve been a DevOps &amp; Infra Engineer for over 5 years. To me, it’s about helping developers move from code to production in the fastest, safest, most secure, and most reliable way possible. The job market seems a bit less certain about this definition however…&#10;Scrolling through job listings this past month, I noticed roles titled DevOps Engineer or Platform Engineer or Infrastructure Engineer that were listed with almost identical responsibilities.&#10;</description><content:encoded><![CDATA[<p>I&rsquo;ve been a DevOps &amp; Infra Engineer for over 5 years. To me, it&rsquo;s about helping developers move from code to production in the fastest, safest, most secure, and most reliable way possible. The job market seems a bit less certain about this definition however&hellip;</p>
<p>Scrolling through job listings this past month, I noticed roles titled <em>DevOps Engineer</em> or <em>Platform Engineer</em> or <em>Infrastructure Engineer</em> that were listed with almost identical responsibilities.</p>
<p>I know innately that these roles have some differences, but the titles are often used interchangeably by hiring teams.</p>
<h2 id="where-devops-came-from" class="group relative scroll-mt-20">
  Where DevOps came from
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#where-devops-came-from"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>DevOps is a word created from the combination of the words <strong>Developer</strong> and <strong>Operations</strong>. To understand why that portmanteau word became a job title, you need to understand the problem it was trying to solve.</p>
<p>Traditionally, software teams were split in two. Developers wrote code in one room, and an <strong>Operations</strong> team —sometimes called Ops— deployed and ran that code in production. Each group had different incentives e.g. devs wanted to ship fast whilst ops wanted stability. That tension caused friction, slow releases, and potentially a blame culture when things broke.</p>
<p>The DevOps movement, popularised in part by <em>The Phoenix Project</em> (Kim, Behr, Spafford, 2013) and <em>The DevOps Handbook</em>, proposed a cultural fix to tear down that wall. If developers owned their code all the way to production, they&rsquo;d build with operability in mind. They&rsquo;d care about deployment pipelines, alerting, and recovery because those things would become their problem too.</p>
<p>DevOps is meant to be a shared practice of principles across teams, and not a single person&rsquo;s role. But organisations found it easier to spin up a DevOps teams or hire someone with the DevOps title who embodied that DevOps mindset to show some adoption of &ldquo;DevOps&rdquo; when it was first cool.</p>
<h2 id="what-a-devops-engineer-actually-does" class="group relative scroll-mt-20">
  What a DevOps Engineer actually does
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#what-a-devops-engineer-actually-does"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>In practice, the <strong>DevOps Engineer</strong> tends to be embedded within a product team, sometimes as the only person with that focus. Their job is improving the speed and safety of getting code from a developer&rsquo;s machine into production.</p>
<p>That means CI/CD pipelines (think GitHub Actions, TeamCity, CircleCI, or similar), automated testing, deployment automation, monitoring and alerting, and security gates. If a developer is waiting too long for a build, or a deployment is a manual, error-prone process, the DevOps Engineer is the one called on.</p>
<p>My own experience working in this role felt exactly like that: a solo DevOps expert embedded in a development team, focused on making the production path faster and more reliable. In smaller companies you will probably get the opportunity to pick up other development tasks outside of the usual job description. This helps you experience the pain that engineers go through to build better solutions.</p>
<h2 id="platform-engineering" class="group relative scroll-mt-20">
  Platform Engineering
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#platform-engineering"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Platform Engineering has similar tools and similar outcomes, but from what I&rsquo;ve seen, the scope and audience are different.</p>
<p>Where a DevOps Engineer typically serves one team, a <strong>Platform Engineer</strong> serves many. This role, I think, is oriented around building an Internal Developer Platform (IDP). An IDP is a curated set of tools, templates, and standards that every engineering team in an organisation can use. The CNCF communities often describe this as building <strong>golden paths</strong>: opinionated, well-maintained routes that make the right thing the easy thing for developers to do.</p>
<p>Think of it as the difference between solving a problem for your team versus building the infrastructure so any team can solve that problem themselves. Platform teams treat internal developers as their customers. They build self-service tooling —things like standardised deployment workflows, observability stacks, or secrets management patterns— so that product teams don&rsquo;t have to reinvent those wheels independently.</p>
<p>This tends to appear at larger organisations, where inconsistency across teams becomes a bigger cost. At a company with ten engineers, one strong DevOps person can handle everything informally. At a company with five hundred, the absence of shared standards can create chaos.</p>
<p>The tradeoff here is somehow creating efficient standards to keep things stable, but also giving optionality to engineers so they have autonomy over the tooling they choose.</p>
<p>A good tool to know about is Spotify&rsquo;s open source Backstage tool: <a href="https://backstage.io/">https://backstage.io/</a></p>
<h2 id="infrastructure-engineering" class="group relative scroll-mt-20">
  Infrastructure Engineering
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#infrastructure-engineering"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Infrastructure Engineering overlaps with both DevOps and Platform, but sits slightly further from application code in my experience.</p>
<p>An <strong>Infrastructure Engineer</strong> is typically focused on the foundations: cloud architecture, networking, access management, and the security controls that sit beneath the application layer. They&rsquo;re thinking about VPCs, subnets, firewall rules, load balancers, DNS, and how services communicate across boundaries. Often this involves deep expertise with IaC tools like Terraform and close familiarity with the cloud provider&rsquo;s lower-level primitives.</p>
<p>A DevOps Engineer might spend their day modifying a GitHub Actions workflow or adjusting a Dockerfile. An Infrastructure Engineer might spend theirs designing the network topology that those pipelines eventually deploy into.</p>
<p>That said, in my experience, in any organisation you have problems to solve and there is never a clean separation. So you&rsquo;re owning all of it, and carrying all three titles simultaneously whenever something is required.</p>
<h2 id="why-the-roles-overlap" class="group relative scroll-mt-20">
  Why the roles overlap
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#why-the-roles-overlap"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>There are more than the three role titles I&rsquo;ve talked about too, e.g. Systems Engineer, Site Reliability Engineer, Integration Engineer, and many others that cover <em>similar</em> responsibilities with decent overlap.</p>
<p>Part of it is because fashionable titles shift over time; DevOps was the go-to term but now it feels like Platform Engineering is. This is probably because it better captures the product-focused, team-of-teams thinking that scales for devops/platform teams.</p>
<p>Each organisation decides on responsibilities differently depending on team size, product maturity, culture in the industry, and even the background of whoever&rsquo;s hiring. So the title they&rsquo;re given is essentially a best guess based on responsibilities required at the start of the role, which often morphs into different roles as projects progress.</p>
<p>A startup with five engineers often doesn&rsquo;t have the headcount for specialisation. The person managing Terraform, building the deployment pipeline, and configuring the VPC is doing DevOps, platform, and infrastructure work all at once.</p>
<p>At larger companies, the separation becomes more meaningful. A Platform team with a product mindset and a roadmap looks very different from a DevOps Engineer embedded in a squad, even if both are deep into Kubernetes on any given Tuesday.</p>
<h2 id="read-the-job-description" class="group relative scroll-mt-20">
  Read the job description
  <a
    href="https://westernwilson.com/reflections/devops-vs-platform-vs-infrastructure-engineer/#read-the-job-description"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>After reading many applications, the practical lesson is to read past the title and into the responsibilities. A &ldquo;Platform Engineer&rdquo; role at a thirty-person startup will look nothing like the same title at an enterprise company. A &ldquo;DevOps Engineer&rdquo; role at a bank might be closer to what a Platform team does at a tech company.</p>
<p>The questions worth asking in an interview: Are you embedded in a product team or sitting in a centralised infrastructure group? Are you building tooling <em>for</em> developers, or helping productionise a single product app? What does ownership look like when something breaks at 3am?</p>
<p>I like that DevOps Engineering is broad; I like having the option to do whatever is required to get the job done.</p>
]]></content:encoded></item><item><title>Why AWS SSO? Why OIDC?</title><link>https://westernwilson.com/reflections/oidc/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/oidc/</guid><description>While building a demo coffee app you realise that the infrastructure-as-code needs appropriate authentication &amp; authorisation to make changes securely. I’ve set this up in previous jobs but have been on a travel break since then, and the muscle memory needs a reminder.&#10;So, I’m refreshing my understanding and reflecting on it here.&#10;Important terminology and tools An important distiction is the difference between Authentication and Authorisation. Authentication asks “are you who you say you are”, while Authorisation says “I know you, but are you allowed to do what you’re trying to do”.&#10;</description><content:encoded><![CDATA[<p>While building a demo coffee app you realise that the infrastructure-as-code needs appropriate authentication &amp; authorisation to make changes securely. I&rsquo;ve set this up in previous jobs but have been on a travel break since then, and the muscle memory needs a reminder.</p>
<p>So, I&rsquo;m refreshing my understanding and reflecting on it here.</p>
<h2 id="important-terminology-and-tools" class="group relative scroll-mt-20">
  Important terminology and tools
  <a
    href="https://westernwilson.com/reflections/oidc/#important-terminology-and-tools"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>An important distiction is the difference between Authentication and Authorisation. <strong>Authentication</strong> asks &ldquo;<em>are you who you say you are</em>&rdquo;, while <strong>Authorisation</strong> says &ldquo;<em>I know you, but are you allowed to do what you&rsquo;re trying to do</em>&rdquo;.</p>
<p><strong>Infrastructure-as-code (IaC)</strong> is self-explanatory, it allows you to codify your infrastructure changes. Letting you track infra changes over time, and efficiently keep the infra actually deployed in line with your intentions. When there is drift between intentions and actual, IaC helps it realign.</p>
<p><strong>Terraform</strong> is a great tool for IAC, and is the focus for this reflection.</p>
<p>There are many other tools that help with IaC including <strong>CloudFormation</strong> and <strong>Ansible</strong>. <strong>CloudFormation</strong> is AWS&rsquo;s solution for the IaC giving declarative provisioning, but its locked to AWS. <strong>Ansible</strong> is more focused on configuration management and orchestration once infrastructure exists, though there is some overlap with IaC.</p>
<p><strong>CI/CD</strong> is Continuous Integration / Continuous Delivery. It is a mindset with many tools that helps our code build, test, and deploy safely for our users. <strong>GitHub Actions</strong> our CI/CD tool of choice. There are others like TeamCity, Circle CI, Jenkins, Octopus Deploy, GitLab CI etc.</p>
<p>When mentioning <strong>MacBook</strong> it could be any developer machine enabling you to make infra changes by running Terraform.</p>
<p>Given all of this, there are two main use cases:</p>
<ul>
<li><strong><em>Human to machine</em></strong> - Connecting MacBook to AWS</li>
<li><strong><em>Machine to machine</em></strong> - Connecting GitHub Actions to AWS</li>
</ul>
<p>Both use cases need to authenticate and authorise, but they do it using different methods. We&rsquo;ll explore that difference in the upcoming sections.</p>
<h2 id="human-to-machine" class="group relative scroll-mt-20">
  Human to machine
  <a
    href="https://westernwilson.com/reflections/oidc/#human-to-machine"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>When running Terraform from your laptop, the industry standard is to authenticate with <strong>AWS SSO</strong> (rebranded as AWS IAM Identity Center in 2022, though the CLI commands remain the same).</p>
<p>From the terminal you run an aws cli command that opens your browser, asks you to log in using your identity provider (or aws creds), and grabs a short-lived credential for use during your AWS terminal session. It normally expires after 8 hours, but this is configurable. Terraform picks up that short-lived credential and uses it to configure the infrastructure however you&rsquo;ve defined.</p>
<div class="highlight"><pre tabindex="0"><code class="language-zsh" data-lang="zsh"><span><span><span># day to day example (assumes sso roles are setup)</span>
</span></span><span><span>aws sso login --profile terraform
</span></span><span><span>export AWS_PROFILE<span>=</span>terraform
</span></span><span><span>
</span></span><span><span>terraform apply
</span></span></code></pre></div><p>These commands work because there&rsquo;s a human in the loop. Developers can login to AWS, click a button, and complete an MFA prompt. It&rsquo;s verifiable that you are the person at the keyboard.</p>
<p>This is not possible the moment you&rsquo;re out of the loop.</p>
<h2 id="machine-to-machine" class="group relative scroll-mt-20">
  Machine to machine
  <a
    href="https://westernwilson.com/reflections/oidc/#machine-to-machine"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>A GitHub Actions CI/CD pipeline can&rsquo;t easily open a browser or tap an allow button on your phone. It needs to authenticate to AWS without a person in the loop, securely, and without any of the interactive trust mechanisms that make SSO work locally.</p>
<p>The old answer was to generate an AWS access key pair, paste it into GitHub as a secret, and let the pipeline use that to authenticate with AWS. This still works, but it&rsquo;s a liability and no longer recommended. Long-lived access keys, if leaked, can grant authentication to whoever gets them. Unfortunately, because infra work requires a broader set of permissions, losing these keys is a bigger security risk.</p>
<p>Long-lived keys should also be <em>rotated</em> frequently (i.e. rotating means to update the keys). That requires additional reminder scheduling, and if the key is used in many places, the work to update might require a bit more effort.</p>
<p>The better answer is Open ID Connect (OIDC). To understand OIDC, you have to understand JWTs first.</p>
<h2 id="jwts" class="group relative scroll-mt-20">
  JWTs
  <a
    href="https://westernwilson.com/reflections/oidc/#jwts"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>JWT stands for JSON Web Token. It&rsquo;s a compact, self-contained token for passing claims between parties.</p>
<p>A <strong>claim</strong> is a key-value assertion about a subject, e.g. <code>&quot;sub&quot;: &quot;user_123&quot;</code> or <code>&quot;email&quot;: &quot;john@example.com&quot;</code>. The issuer party (AWS or GitHub) is saying: I assert these properties are true about the subject of this token (and that it is valid for the stated audience and time window)</p>
<p>What might be at first surprising, is that a signed JWT isn&rsquo;t encrypted and anyone can read it. Paste a JWT string into <a href="https://jwt.io">jwt.io</a> and you&rsquo;ll see everything inside (with real tokens, keep them out of public tools!).</p>
<p>The point of JWT tokens isn&rsquo;t secrecy, it&rsquo;s integrity. If you trust the issuer&rsquo;s public key, and the signature checks out, then you can trust the claims.</p>
<p>Think of it like a driver&rsquo;s licence, it&rsquo;s contents are not a secret (anyone who physically holds it has the info), but it&rsquo;s validity is trusted because it was created by a trusted authority. Other parties (police, security, the supermarket cashier) recognise it and trust what it <em>claims</em>.</p>
<p>A JWT is a string with three parts separated by dots. Header, payload, and signature.</p>
<div class="highlight"><pre tabindex="0"><code class="language-zsh" data-lang="zsh"><span><span>&lt;header&gt;.&lt;payload&gt;.&lt;signature&gt;
</span></span></code></pre></div><p>The <em>header</em> says how it was signed. The <em>payload</em> is a set of claims or facts about whoever the token is describing. The <em>signature</em> is the issuer&rsquo;s cryptographic stamp.</p>
<p>E.g.</p>
<div class="highlight"><pre tabindex="0"><code class="language-zsh" data-lang="zsh"><span><span>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXJpb3VzX3JlYWRlciIsIm5hbWUiOiJZb3UsIHRoZSBjdXJpb3VzIG9uZSIsIm1lc3NhZ2UiOiJUaGV5IHNheSBjdXJpb3NpdHkga2lsbGVkIHRoZSBjYXQsIGJ1dCBpdCB3b24ndCBraWxsIHlvdS4gU3RheSBjdXJpb3VzLiIsImlhdCI6MTc0NjY2MjQwMCwiZXhwIjo5OTk5OTk5OTk5fQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
</span></span></code></pre></div><p>You can read more detail here: <a href="https://www.jwt.io/introduction#what-is-json-web-token">https://www.jwt.io/introduction#what-is-json-web-token</a></p>
<h2 id="oidc" class="group relative scroll-mt-20">
  OIDC
  <a
    href="https://westernwilson.com/reflections/oidc/#oidc"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>OIDC (OpenID Connect) is how JWTs get used for authentication. An identity provider mints a signed JWT about who someone is or what it&rsquo;s doing, and a relying party verifies the token and decides what to do.</p>
<p>Back to the analogy of a JWT and a drivers license. The issuing authority is whoever grants drivers licenses, in New Zealand this would be NZ Transport Agency (NZTA or Waka Kotahi). The relying party, again, could be police, security, or the supermarket cashier.</p>
<p>For your CI/CD, GitHub is the identity provider and AWS is the relying party. GitHub publishes its public keys at a well-known URL. AWS knows where to find them and when the workflow pipeline runs, GitHub mints a JWT describing that specific job (the repo, git branch, the workflow file) and signs it. The pipeline hands the token to AWS, AWS verifies the signature, reads the claims, and decides whether to issue short-lived credentials.</p>
<p>There&rsquo;s no shared secret and no pre-arranged handshake. You declare in your AWS account that you trust GitHub&rsquo;s issuer URL and from that moment, AWS knows how to verify any token GitHub signs.</p>
<p>This grants you authentication, and next it&rsquo;s important to look at the policies that grant authorisation to make changes.</p>
<h2 id="the-trust-policy" class="group relative scroll-mt-20">
  The trust policy
  <a
    href="https://westernwilson.com/reflections/oidc/#the-trust-policy"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>With the authentication and JWT verification mostly handled by AWS &amp; GitHub, you can focus more on deciding which GitHub claims you&rsquo;ll accept.</p>
<p>For example, every JWT GitHub mints contains a <code>sub</code> claim, i.e. the subject</p>
<div class="highlight"><pre tabindex="0"><code class="language-zsh" data-lang="zsh"><span><span>repo:my-org/my-repo:ref:refs/heads/main
</span></span></code></pre></div><p>That string identifies exactly which repo, branch, or trigger produced the token. Your trust policy in AWS is a set of conditions on those claims. So if you pin the <code>sub</code> claim to a specific repo and your main branch (like the above example), then only that combination can assume the role.</p>
<p>Set up the AWS trust policy too loosely and you replace one risk with another. The biggest mistake is wildcarding the subject <code>repo:*</code>, which means that any repository on GitHub could potentially assume your role. The fix is to only give access to specific repositories that need it, following least privilege access. Most tutorials highlight this point too.</p>
<h2 id="using-github-environments" class="group relative scroll-mt-20">
  Using GitHub environments
  <a
    href="https://westernwilson.com/reflections/oidc/#using-github-environments"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Many tutorials suggest the use of GitHub Environments for production deploys. So you can pin the trust policy to <code>repo:my-org/my-repo:environment:production</code>. You then set up your repo to require manual approval on production deployments, thus adding another layer of protection. Now even a workflow on main can&rsquo;t deploy until a human clicks a button.</p>
<p>For an enterprise team, this makes sense. Multiple people merging code, the reviewer and the deployer might not be the same person. So the approval gate gives you a real second pair of eyes.</p>
<p>For a solo project without actual customers it&rsquo;s optional, but worth knowing the pattern exists for when the team grows.</p>
<h2 id="relearning" class="group relative scroll-mt-20">
  Relearning
  <a
    href="https://westernwilson.com/reflections/oidc/#relearning"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Coming back to OIDC after a year of travel, I noticed new details since the first time round. Especially in terms of trust policies, and <em>why</em> the <code>sub</code> claim specificity matters. Going back to first principles about JWTs and OIDC before touching any config gave that stronger mental model.</p>
<p>If you want to see how someone might configure all of this, check out my repositories here:</p>
<ul>
<li>AWS Foundation SSO &amp; OIDC setup: <a href="https://github.com/wjkw1/aws-foundations">https://github.com/wjkw1/aws-foundations</a></li>
<li>Demo Application: <a href="https://github.com/wjkw1/devops-profile-coffee-card-app-demo">https://github.com/wjkw1/devops-profile-coffee-card-app-demo</a></li>
</ul>
]]></content:encoded></item><item><title>Deciding what to keep</title><link>https://westernwilson.com/reflections/bunq-hackathon-7/</link><pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/bunq-hackathon-7/</guid><description>It was around 4am when I scrapped the project. AI had co-built me a FastAPI cathedral, full of libraries I didn’t recognise and an architecture more ambitious than a 24 hour hackathon could achieve. The code worked, sort of. Extending it would be challenging, and integrating it with AWS would take time we didn’t have. So I deleted everything and started again with something simple that we could build and demo quickly.&#10;</description><content:encoded><![CDATA[<p>It was around 4am when I scrapped the project. AI had co-built me a FastAPI cathedral, full of libraries I didn&rsquo;t recognise and an architecture more ambitious than a 24 hour hackathon could achieve.
The code worked, sort of. Extending it would be challenging, and integrating it with AWS would take time we didn&rsquo;t have. So I deleted everything and started again with something simple that we could build and demo quickly.</p>
<p>This moment has been circling in my mind across this last week.</p>
<h2 id="how-i-got-there" class="group relative scroll-mt-20">
  How I got there
  <a
    href="https://westernwilson.com/reflections/bunq-hackathon-7/#how-i-got-there"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>I went to Amsterdam for three nights to see the city and to take part in Bunq&rsquo;s 24-hour hackathon. I arrived alone and Bunq connected me with a team. On the day two people didn&rsquo;t show so we ended up finding two others in similar situations and formed a new team.</p>
<p>The hackathon brief was to build something that solved a problem, used the Bunq API, and had some kind of multi-modal AI. I didn&rsquo;t know what we&rsquo;d build, and honestly that was exciting to me!</p>
<h2 id="who-i-worked-with" class="group relative scroll-mt-20">
  Who I worked with
  <a
    href="https://westernwilson.com/reflections/bunq-hackathon-7/#who-i-worked-with"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Our team was called &ldquo;4 corners&rdquo; with people from four countries. We had a DevOps Engineer, Business Analyst, Product Owner, and even a CFO. Honestly, it was a privilege to be around such accomplished people whose lives had taken different trajectories leading them to this moment.</p>
<h2 id="the-build" class="group relative scroll-mt-20">
  The build
  <a
    href="https://westernwilson.com/reflections/bunq-hackathon-7/#the-build"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>On the day we had a brilliant talk by someone from AWS about spec driven development. I took that talk to heart and tried building our app using that same ethos. This led me to a vibe-coded backend app in Python, the language I knew, but attempting to use production-grade infrastructure. For a 24-hour prototype, this was overkill. The AI, eager to please, happily built me something far beyond what the hackathon called for.</p>
<p>It reminds me of something an old colleague once told me:</p>
<blockquote>
<p>building something is easy, productionising it is the hard bit</p>
</blockquote>
<p>In this hackathon, unfortunately, I started off by inverting their advice. I was productionising something before I&rsquo;d built anything.</p>
<h2 id="the-pivot" class="group relative scroll-mt-20">
  The pivot
  <a
    href="https://westernwilson.com/reflections/bunq-hackathon-7/#the-pivot"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>You can wrangle an app together in 24 hours. What matters more is whether you&rsquo;ve thought clearly about the problem, can articulate the pain point, and can demonstrate something useful with honest tradeoffs.</p>
<ul>
<li>Mock APIs not crucial to the core solution instead of integrating with them.</li>
<li>Stub the database or run one locally to show the core idea working.</li>
<li>Later, if there&rsquo;s time then connect the real infra.</li>
</ul>
<p>The lesson for me was closing the gap between ambition and execution. A gentle reminder to focus more on the problem being solved, and on building an MVP that demonstrates a useful solution.</p>
<p>That&rsquo;s what scrapping the bloated AI app got me back to. Something small, something easily understood, something we could actually show someone.</p>
<p>The winning teams that night? Mostly localhost applications.</p>
<h2 id="amsterdam" class="group relative scroll-mt-20">
  Amsterdam
  <a
    href="https://westernwilson.com/reflections/bunq-hackathon-7/#amsterdam"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>After the Hackathon winners were announced, I left Bunq HQ into a city I&rsquo;d been told was lovely. My cousin recommended Amsterdam as the place to be, and I could see why. Flat landscape, bike-friendly, sunny skies that week (rare, apparently), with free ferries criss-crossing the harbour, and people moving through it all with quiet competence. It felt like a larger Wellington with better trains.</p>
<p>I came partly to see whether I could live in Amsterdam. I left knowing I could.</p>
]]></content:encoded></item><item><title>Terraform state s3 bucket naming convention</title><link>https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/</link><pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/</guid><description>While working on my personal devops project (see github), I realised I hadn’t thought through the naming convention of my terraform state in a long time.&#10;Naming convention decision Final name: tfstate-{AccountId}-{GitHubRepo}&#10;Example: tfstate-123456789012-aws-foundations&#10;Why this name The convention earns its structure because I’m committing to one terraform state bucket per project.&#10;</description><content:encoded><![CDATA[<p>While working on my personal devops project (<a href="https://github.com/wjkw1/devops-profile-coffee-card-app-demo">see github</a>), I realised I hadn&rsquo;t thought through the naming convention of my terraform state in a long time.</p>
<h2 id="naming-convention-decision" class="group relative scroll-mt-20">
  Naming convention decision
  <a
    href="https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/#naming-convention-decision"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p><strong>Final name:</strong> <code>tfstate-{AccountId}-{GitHubRepo}</code></p>
<p><strong>Example:</strong> <code>tfstate-123456789012-aws-foundations</code></p>
<h3 id="why-this-name" class="group relative scroll-mt-20">
  Why this name
  <a
    href="https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/#why-this-name"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>The convention earns its structure because I&rsquo;m committing to one terraform state bucket per project.</p>
<p>So, the three pieces of information communicated in the bucket name:</p>
<table>
	<thead>
			<tr>
					<th>Segment</th>
					<th>Reason</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>tfstate</code> <em>prefix</em></td>
					<td>Immediately identifies the bucket&rsquo;s purpose to any observer, without needing to open it.</td>
			</tr>
			<tr>
					<td><code>{AccountId}</code></td>
					<td>The only reliable way to guarantee global S3 uniqueness (bucket names are shared across all of AWS); also identifies ownership at a glance in billing, access logs, and cross-account contexts.</td>
			</tr>
			<tr>
					<td><code>{GitHubRepo}</code></td>
					<td>1:1 mapping between bucket and repository — you can navigate from S3 → GitHub (or back) without any lookup.</td>
			</tr>
	</tbody>
</table>
<p>The convention scales cleanly: every future infrastructure repository gets <code>tfstate-{AccountId}-{GitHubRepo}</code>, with no naming decisions to revisit.</p>
<h3 id="options-considered-and-why-they-were-rejected" class="group relative scroll-mt-20">
  Options considered and why they were rejected
  <a
    href="https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/#options-considered-and-why-they-were-rejected"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p><strong><code>tfstate-aws-foundations</code></strong></p>
<p>Not globally unique, and almost certainly already taken thanks to the known limitation of S3. It also encodes the github project name in a bucket originally conceived as potentially holding all account state. Once my intent changed, the name became misleading.</p>
<p><strong><code>tfstate-{AccountId}</code></strong> <em>(original)</em></p>
<p>Unique and future-proof, but loses the repo link. Fine if one bucket holds state for many projects; less good when the bucket is scoped 1:1 to a single repo, where the link has real navigational value.</p>
<p><strong><code>{AccountId}-aws-foundations</code></strong></p>
<p>Globally unique, still links to the repo. But without a <code>tfstate</code> indicator, anyone browsing S3 has no immediate signal about what the bucket is for. The AccountId prefix also groups infrastructure buckets together when sorted, at the cost of reading unnaturally left-to-right.</p>
<p><strong><code>aws-foundations-{AccountId}</code></strong></p>
<p>More readable than the account-first example, same repo link. Still missing the <code>tfstate</code> indicator.</p>
<p><strong><code>tfstate-{AccountId}-aws-foundations</code></strong> <em>(chosen)</em></p>
<p>Purpose is obvious. Guaranteed to be unique. The 1:1 GitHub mapping is perfect. The tradeoff: twelve digits in the middle makes the name long and not great to say out loud. That cost is low because this string gets typed approximately twice in the bucket&rsquo;s lifetime &ndash; at creation and in <code>versions.tf</code> &ndash; so conversational readability is not important.</p>
<h3 id="convention-going-forward" class="group relative scroll-mt-20">
  Convention going forward
  <a
    href="https://westernwilson.com/reflections/terraform-state-s3-bucket-naming-convention/#convention-going-forward"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p><code>tfstate-{AccountId}-{GitHubRepo}</code></p>
<p>Other projects I create that require aws infra will each get their own S3 state bucket following this pattern.</p>
<p>This gives me state isolation between projects; a terraform mistake in one project cannot affect another, and state files stay small and fast.</p>
]]></content:encoded></item><item><title>Linting and formatting code</title><link>https://westernwilson.com/reflections/linting-and-formatting/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/linting-and-formatting/</guid><description>TL;DR: Code style and formatting should not steal your brainpower. Set up linting/formatting at three touchpoints (your editor, your pre-commit hook, and your CI pipeline) and formatting becomes automatic from the moment you write code to the moment it merges. At the end, this reflection walks through exactly how to do that for a Python FastAPI project.&#10;Linting and formatting code Recently, while working on a personal DevOps project, I spent some time looking into linting tools. They amazingly save your brainpower for the actual problem, instead of code styling. Whether it’s two spaces or four per indent, single quotes or double, I don’t mind. Other people have thought deeply about these trade-offs, landed on reasonable answers, and codified them. I’m happy to piggyback on that.&#10;</description><content:encoded><![CDATA[<p><strong>TL;DR:</strong> Code style and formatting should <strong>not</strong> steal your brainpower. Set up linting/formatting at three touchpoints (your editor, your pre-commit hook, and your CI pipeline) and formatting becomes automatic from the moment you write code to the moment it merges. At the end, this reflection walks through exactly how to do that for a Python <a href="https://fastapi.tiangolo.com">FastAPI</a> project.</p>
<h2 id="linting-and-formatting-code" class="group relative scroll-mt-20">
  Linting and formatting code
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#linting-and-formatting-code"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Recently, while working on a <a href="https://github.com/wjkw1/devops-profile-coffee-card-app-demo">personal DevOps project</a>, I spent some time looking into linting tools. They amazingly save your brainpower for the actual problem, instead of code styling. Whether it&rsquo;s two spaces or four per indent, single quotes or double, I don&rsquo;t mind. Other people have thought deeply about these trade-offs, landed on reasonable answers, and codified them. I&rsquo;m happy to piggyback on that.</p>
<p>I aim to write code that works, and let the styling happen automatically.</p>
<p>There&rsquo;s a side benefit too: when a codebase consistently uses a popular linting tool, anyone familiar with it can scan the code and immediately know where to look. It&rsquo;s like how headings in a research paper let you scan before you commit to reading.</p>
<h2 id="three-touchpoints-that-matter" class="group relative scroll-mt-20">
  Three touchpoints that matter
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#three-touchpoints-that-matter"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Good linting hygiene should show up at meaningful points in your development lifecycle:</p>
<ol>
<li><strong>In your IDE (or text editor), as you write</strong></li>
<li><strong>Locally, before you commit</strong></li>
<li><strong>In CI/CD, before code merges</strong></li>
</ol>
<p>Each layer serves a different purpose. Together, they make code quality essentially automatic. (Tools like <a href="https://www.sonarsource.com/products/sonarqube/">SonarQube</a> go deeper —security, coverage, and code smells at a project level— but that&rsquo;s a reflection for another time.)</p>
<figure class="img-light"><img src="https://westernwilson.com/reflections/linting-and-formatting/linting_touchpoints_light.svg"
			alt="Three linting touchpoints">
</figure>

<figure class="img-dark"><img src="https://westernwilson.com/reflections/linting-and-formatting/linting_touchpoints_dark.svg"
			alt="Three linting touchpoints">
</figure>

<p><strong>In the IDE (or text editor)</strong>, I use <a href="https://code.visualstudio.com">VS Code</a> with the relevant extensions installed and Format on Save enabled. It does what it sounds like: when the file saves, the formatter runs. Sometimes this needs dev dependencies installed to work properly; check the official docs and you&rsquo;ll figure it out quickly, I&rsquo;ll go into <a href="https://docs.astral.sh/ruff/">Ruff</a> for Python specifically later in this post.</p>
<p><strong>Locally, before committing</strong>, is where things get interesting. At various companies I&rsquo;ve seen the same pattern repeat: developer forgets to format, pushes code, CI fails, they fix locally, force-push, CI reruns. This adds ten minutes to the dev cycle, minimum. Multiply that across a team and it adds up quickly.</p>
<p>Git&rsquo;s <a href="https://pre-commit.com">pre-commit</a> hooks solve this cleanly. When you attempt a commit, a hook runs your formatting and linting checks first. If they fail, the commit doesn&rsquo;t go through. This means no broken CI and no annoying formatting fix commits. This enables the code to validate itself before it ever leaves your machine.</p>
<p>The one catch: developers can skip the setup step entirely, which is exactly why the third layer exists.</p>
<p><strong>In CI/CD</strong>, the pipeline enforces what the pre-commit hook encourages. Even if someone bypasses local hooks, nothing merges unless the linting passes. This is your actual quality gate and is what you rely on.</p>
<h2 id="why-ruff-instead-of-black-and-flake8" class="group relative scroll-mt-20">
  Why Ruff instead of Black and Flake8
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#why-ruff-instead-of-black-and-flake8"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Three or more years ago, I used <a href="https://black.readthedocs.io/en/stable/">Black</a> for formatting and <a href="https://flake8.pycqa.org/en/latest/">Flake8</a> for static analysis. Two tools, two configs, two things to install and maintain. It was a minor friction, but that friction compounded and was a little naff.</p>
<p>Ruff replaces both with a single tool. It&rsquo;s faster, simpler, and opinionated enough that you don&rsquo;t have to make many decisions. For a project where the point is <em>less thinking about styling and formatting</em>, that&rsquo;s exactly what I wanted.</p>
<h2 id="the-setup-python--fastapi" class="group relative scroll-mt-20">
  The Setup (Python / FastAPI)
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#the-setup-python--fastapi"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h2>
<p>Three things to configure, in roughly this order:</p>
<h3 id="step-1-vs-code--ruff-extension" class="group relative scroll-mt-20">
  Step 1: VS Code + Ruff extension
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#step-1-vs-code--ruff-extension"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>Install the <a href="https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff">Ruff extension</a> from the VS Code marketplace — the official one is published by Astral (charliermarsh.ruff). Once installed, add this to your .vscode/settings.json:</p>
<div class="highlight"><pre tabindex="0"><code class="language-json" data-lang="json"><span><span>{
</span></span><span><span>  <span>&#34;[python]&#34;</span>: {
</span></span><span><span>    <span>&#34;editor.formatOnSave&#34;</span>: <span>true</span>,
</span></span><span><span>    <span>&#34;editor.defaultFormatter&#34;</span>: <span>&#34;charliermarsh.ruff&#34;</span>,
</span></span><span><span>    <span>&#34;editor.codeActionsOnSave&#34;</span>: {
</span></span><span><span>      <span>&#34;source.fixAll.ruff&#34;</span>: <span>&#34;explicit&#34;</span>,
</span></span><span><span>      <span>&#34;source.organizeImports.ruff&#34;</span>: <span>&#34;explicit&#34;</span>
</span></span><span><span>    }
</span></span><span><span>  }
</span></span><span><span>}
</span></span></code></pre></div><p>Committing <code>.vscode/settings.json</code> to the repo is intentional. Anyone who clones the project and opens it in VS Code gets the same formatting behaviour automatically. No extra knowledge required.</p>
<p>No separate Ruff installation is needed as the VS Code extension ships its own binary, and the pre-commit hook (added in Step 2) bundles its own too. You just need to add your Ruff config to <code>ruff.toml</code>:</p>
<div class="highlight"><pre tabindex="0"><code class="language-toml" data-lang="toml"><span><span>[<span>tool</span>.<span>ruff</span>]
</span></span><span><span><span>line-length</span> = <span>88</span>
</span></span><span><span><span>target-version</span> = <span>&#34;py312&#34;</span>
</span></span><span><span>
</span></span><span><span>[<span>tool</span>.<span>ruff</span>.<span>lint</span>]
</span></span><span><span><span>select</span> = [<span>&#34;E&#34;</span>, <span>&#34;F&#34;</span>, <span>&#34;I&#34;</span>]
</span></span></code></pre></div><p>The select block opts into pycodestyle errors (E), Pyflakes (F), and isort import sorting (I). This covers what you&rsquo;d previously have needed Black, Flake8, and <a href="https://pycqa.github.io/isort/">isort</a> to do separately.</p>
<h3 id="step-2-pre-commit-hooks" class="group relative scroll-mt-20">
  Step 2: Pre-commit hooks
  <a
    href="https://westernwilson.com/reflections/linting-and-formatting/#step-2-pre-commit-hooks"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>First, install the pre-commit package:</p>
<div class="highlight"><pre tabindex="0"><code class="language-bash" data-lang="bash"><span><span>pip install pre-commit
</span></span></code></pre></div><p>Then create a <code>.pre-commit-config.yaml</code> in the root of your repo. Here&rsquo;s exactly what&rsquo;s in the project:</p>
<div class="highlight"><pre tabindex="0"><code class="language-yaml" data-lang="yaml"><span><span><span>repos</span>:
</span></span><span><span>  - <span>repo</span>: <span>https://github.com/astral-sh/ruff-pre-commit</span>
</span></span><span><span>    <span>rev</span>: <span>v0.11.4</span>
</span></span><span><span>    <span>hooks</span>:
</span></span><span><span>      - <span>id</span>: <span>ruff</span>
</span></span><span><span>        <span>args</span>: [--<span>fix]</span>
</span></span><span><span>        <span>files</span>: <span>^api/</span>
</span></span><span><span>      - <span>id</span>: <span>ruff-format</span>
</span></span><span><span>        <span>files</span>: <span>^api/</span>
</span></span></code></pre></div><p>A few things worth noting here. The files: <code>^api/</code> pattern scopes the hooks to the API directory only — the frontend has its own linting story (<a href="https://eslint.org">ESLint</a> + <a href="https://prettier.io">Prettier</a>), so there&rsquo;s no point running Ruff across the whole repo. The ruff hook handles lint checks and the <code>--fix</code> flag means it&rsquo;ll auto-fix what it can rather than just reporting issues. The ruff-format hook handles formatting, equivalent to what Black would have done.
Once the file is in place, install the hooks:</p>
<div class="highlight"><pre tabindex="0"><code class="language-bash" data-lang="bash"><span><span>pre-commit install
</span></span></code></pre></div><p>This wires the hooks into your local <code>.git/hooks/pre-commit</code>. From this point, every time you run git commit, Ruff runs first. If it finds issues it can&rsquo;t auto-fix, the commit is blocked and the problems are printed to the terminal. Fix them, stage the changes, and commit again.
You can also run it manually across all files at any point:</p>
<div class="highlight"><pre tabindex="0"><code class="language-bash" data-lang="bash"><span><span>pre-commit run --all-files
</span></span></code></pre></div><p>Useful for a first run after setting up, or after pulling in changes from someone else who hasn&rsquo;t had the hooks installed.</p>
<p>That&rsquo;s it. Once it&rsquo;s done, your automatic project linting works in the background. Which is perfect.</p>
]]></content:encoded></item><item><title>C4 Models for software system diagrams</title><link>https://westernwilson.com/reflections/c4models/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://westernwilson.com/reflections/c4models/</guid><description>In 2022, a colleague introduced me to the C4 Model framework for architecture diagrams. At the time, I’d seen nothing like it. I’d often been in whiteboard sessions, struggling to make sense of all the boxes and lines people drew. Diagrams were drawn completely different depending on the person holding the marker. This colleague was a star communicator and an inspiring solutions architect. Their ability to convey complex ideas clearly was a huge influence in me picking up the framework and running with it for my own diagrams.&#10;</description><content:encoded><![CDATA[<p>In 2022, a colleague introduced me to the C4 Model framework for architecture diagrams. At the time, I&rsquo;d seen nothing like it. I&rsquo;d often been in whiteboard sessions, struggling to make sense of all the boxes and lines people drew. Diagrams were drawn completely different depending on the person holding the marker.
This colleague was a star communicator and an inspiring solutions architect. Their ability to convey complex ideas clearly was a huge influence in me picking up the framework and running with it for my own diagrams.</p>
<h3 id="what-is-the-c4-model" class="group relative scroll-mt-20">
  What is the C4 Model?
  <a
    href="https://westernwilson.com/reflections/c4models/#what-is-the-c4-model"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>The C4 Model is a structured, uniform approach to describe the architecture of a software system at different level of detail. Instead of throwing everything onto a single diagram and hoping people get it, it organises abstractions in a way that mirrors how developers or software architects tend to think about systems.</p>
<p>The 4 levels are:</p>
<ol>
<li>System Context - the big picture: your system and how it interacts with users and other systems.</li>
<li>Containers - the applications and data stores that make up your system.</li>
<li>Components - the building blocks of each of our containers.</li>
<li>Code - the classes, interfaces, and functions that implement each component.
The official definition explains it best:
<blockquote>
<p>A software system is made up of one or more containers (applications and data stores), each of which contains one or more components, which in turn are implemented by one or more code elements (classes, interfaces, objects, functions, etc). And people (actors, roles, personas, named individuals, etc) use the software systems that we build.
Source: <a href="https://arc.net/l/quote/kvovhsbn">https://arc.net/l/quote/kvovhsbn</a>
Visit the official website to read more detail about this framework.</p>
</blockquote>
</li>
</ol>
<h3 id="the-map-is-not-the-territory" class="group relative scroll-mt-20">
  The map is not the territory
  <a
    href="https://westernwilson.com/reflections/c4models/#the-map-is-not-the-territory"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>A book called The Great Mental Models: General Thinking Concepts has a chapter called &ldquo;the map is not the territory&rdquo;. The idea being that the granularity of Google Maps, does not show the same level of detail as someone walking around on the ground. This is a similar line of reasoning useful when thinking about the C4 diagram.
For most intents and purposes, the maps context is enough to be useful at a high-level. But sometimes, we need more detail to understand what is going on under the hood. That&rsquo;s why we have these layers of abstraction, and at each abstraction we are targeting a different audience.</p>
<h3 id="the-caveat-at-the-code-level" class="group relative scroll-mt-20">
  The caveat at the code level
  <a
    href="https://westernwilson.com/reflections/c4models/#the-caveat-at-the-code-level"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>The creator of this framework, Simon Brown, recommends never manually building the lowest level C4 class diagram. It&rsquo;s not worth the effort of building, and definitely not worth keeping updated; especially with the pace at which software changes nowadays. The level of detail is normally so closely related to code that it can (and should) be generated automatically.
This is a pragmatic acknowledgement that diagrams are tools, not artefacts to be maintained for their own sake. If a diagram can&rsquo;t keep pace with reality, it quickly becomes a source of confusion rather than clarity.</p>
<h3 id="examples-from-a-personal-project" class="group relative scroll-mt-20">
  Examples from a personal project
  <a
    href="https://westernwilson.com/reflections/c4models/#examples-from-a-personal-project"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h3>
<p>I&rsquo;ve been applying this framework in a recent pet project. If you&rsquo;d like to see the diagrams in action, head over to the demo project here: devops-profile-coffee-card-app-demo.</p>
<h4 id="context" class="group relative scroll-mt-20">
  Context
  <a
    href="https://westernwilson.com/reflections/c4models/#context"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h4>
<p><img src="https://westernwilson.com/reflections/c4models/CoffeeCards-C4-Context.jpg" alt="C4 Context Diagram"></p>
<h4 id="containers" class="group relative scroll-mt-20">
  Containers
  <a
    href="https://westernwilson.com/reflections/c4models/#containers"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h4>
<p><img src="https://westernwilson.com/reflections/c4models/CoffeeCards-C4-Container.jpg" alt="C4 Container Diagram"></p>
<h4 id="components-tbd" class="group relative scroll-mt-20">
  Components TBD
  <a
    href="https://westernwilson.com/reflections/c4models/#components-tbd"
    class="heading-anchor ml-2 text-sm no-underline! opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100 [color:var(--c-muted)] hover:[color:var(--c-accent)] transition-opacity"
    aria-label="Link to this heading"
  ><i class="fa-solid fa-link" aria-hidden="true"></i></a>
</h4>
<p><em>Coming soon&hellip;</em></p>
]]></content:encoded></item></channel></rss>