<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Applied Computing Research Labs]]></title><description><![CDATA[Open-source research and development in distributed systems]]></description><link>https://blog.appliedcomputing.io</link><image><url>https://substackcdn.com/image/fetch/$s_!M8wv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdbcc8-dc09-4e12-8fc3-348cb9c2691e_518x518.png</url><title>Applied Computing Research Labs</title><link>https://blog.appliedcomputing.io</link></image><generator>Substack</generator><lastBuildDate>Thu, 15 Oct 2026 14:46:37 GMT</lastBuildDate><atom:link href="https://blog.appliedcomputing.io/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[David Morrison]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[drmorr@appliedcomputing.io]]></webMaster><itunes:owner><itunes:email><![CDATA[drmorr@appliedcomputing.io]]></itunes:email><itunes:name><![CDATA[drmorr]]></itunes:name></itunes:owner><itunes:author><![CDATA[drmorr]]></itunes:author><googleplay:owner><![CDATA[drmorr@appliedcomputing.io]]></googleplay:owner><googleplay:email><![CDATA[drmorr@appliedcomputing.io]]></googleplay:email><googleplay:author><![CDATA[drmorr]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Key Autoscaling Metric #3: Error Rates and Termination Grace Periods]]></title><description><![CDATA[This is the third post in our Five Key Autoscaling Metrics series; in the first post, Ian broke down the Workload Committed Capacity metric, or &#8220;why your utilization isn&#8217;t as good as you think it is&#8221;.]]></description><link>https://blog.appliedcomputing.io/p/key-autoscaling-metric-3-error-rates</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/key-autoscaling-metric-3-error-rates</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 09 Oct 2026 22:01:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JF0G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JF0G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JF0G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JF0G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png" width="900" height="900" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e32f1095-7d3d-4121-a78d-459829d3c762_900x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:900,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:143458,&quot;alt&quot;:&quot;A purple octopus holding a sign that says \&quot;my autoscalar\&quot;, and is covered with boxes and arrows going every which way&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/219649613?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A purple octopus holding a sign that says &quot;my autoscalar&quot;, and is covered with boxes and arrows going every which way" title="A purple octopus holding a sign that says &quot;my autoscalar&quot;, and is covered with boxes and arrows going every which way" srcset="https://substackcdn.com/image/fetch/$s_!JF0G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!JF0G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe32f1095-7d3d-4121-a78d-459829d3c762_900x900.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Mr. Squidler brought his own whiteboard to his system design interview, does that get him bonus points?</figcaption></figure></div><p>This is the third post in our <a href="https://blog.appliedcomputing.io/p/five-key-autoscaling-metrics-for">Five Key Autoscaling Metrics</a> series; in the <a href="https://blog.appliedcomputing.io/p/key-autoscaling-metric-1-workload">first post</a>, Ian broke down the Workload Committed Capacity metric, or &#8220;why your utilization isn&#8217;t as good as you think it is&#8221;. <a href="https://blog.appliedcomputing.io/p/key-autoscaling-metric-2-mean-time">Last week</a>, I talked about measuring the &#8220;scale-up&#8221; performance of your autoscaler from the perspective of your users: namely, how long they have to wait before they can see their AI-generated cat pics. This week, we&#8217;re going to look at the inverse: how much your users notice when your autoscaler scales down (and why Kubernetes makes it virtually guaranteed that your users <em>will</em> notice).</p><h2>Can you please design an autoscaler on the whiteboard?</h2><p>If I&#8217;ve learned anything from doing endless architecture and/or system-design interviews (on both sides of the table), it&#8217;s that what software development looks like in an interview has <em>nothing at all</em> to do with what software development looks like in real life. See, in an interview, typically you&#8217;re asked to &#8220;design a system&#8221; for the interviewer, who can then ask you arbitrary questions like &#8220;how many requests per second can your whiteboard handle?&#8221; In real software engineering, &#8220;design&#8221; is a naughty word and most people skip right past it. But I digress; just for the sake of argument, let&#8217;s suppose that somebody asked us in an interview to design an autoscaler that gracefully scales down. How would you do it?</p><p>Well, let&#8217;s think about what can go wrong on scale-down: if your pod isn&#8217;t doing anything, you can just kill it. If it <em>is</em> doing something, then maybe you should let it finish before you shut it down. But how do you know if your pod is doing something?</p><p>Spoiler warning: you don&#8217;t. You&#8217;re the platform, you have <em>literally no idea</em> what kinds of applications are running on top of the platform, so it is literally impossible for you to understand whether it&#8217;s safe to terminate your application. So anyways, I guess we&#8217;re going to fail this system design interview, because most interviewers don&#8217;t like being told &#8220;the problem you&#8217;re asking me to solve is impossible&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>Just kidding, we actually <em>can</em> do something: we could (<em>gasp</em>) ask the application whether or not it&#8217;s ready to be terminated<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>! And then we could give it some kind of, idk, &#8220;<a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow">termination grace period</a>&#8221; or something so that the application has a reasonable amount of time to shut down but it can&#8217;t greedily just hang on to its resources forever.</p><p>So, putting my interviewer hat back on for a minute, I say &#8220;Huh. That seems like a reasonable approach, but how can you tell whether it&#8217;s successful? In other words, how do you know whether your autoscaling policy is actually impacting your user experience?&#8221;</p><p>And then I take my interviewer hat off and I respond &#8220;Users? What are those? I&#8217;m a platform engineer, I don&#8217;t care about the users!&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><h2>I thought this post was supposed to be about metrics</h2><p>Ok, I&#8217;m kindof burying the lede in this post<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, but the actual answer to the hypothetical interviewer&#8217;s question is, &#8220;You measure it!&#8221; Specifically, you need to understand a) what kind of errors your application can throw, and b) how many of them are happening. In some cases, this is easy: is it a webserver? Then it probably throws a 500 error when the HTTP request abruptly gets canceled. Is it some kind of queue processor? Maybe the task it was working on ends up in a dead letter queue. In other cases, you may need to think a little bit more creatively about your application&#8217;s errors and how to measure them.</p><p>But! It&#8217;s not sufficient to just &#8220;count errors&#8221;, because your application can throw errors for any number of different reasons. What you <em>really</em> want to be able to do (because this is all about autoscaling, remember) is connect the errors that get thrown to the actions taken by your autoscaler. Seems easy, right<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>?</p><p>Welllllll it turns out that in <em>today&#8217;s</em> world, about the best you can do is correlation, not causation. You &#8220;probably&#8221; have some metrics getting emitted about your <code>5xx</code> response rate for various pods, and you might also have some metrics from your autoscaler indicating whether it took a scale-down action. You can then do some extremely ugly PromQL to apply some kind of envelope function to your error rates metric, so that you can produce a count over time of &#8220;errors occurring during scaledown&#8221;.</p><p>Normally I would try to be helpful and actually tell you what that PromQL is, but that honestly sounds harrowing enough that I don&#8217;t want to try, and I give you 10-to-1 odds that your monitoring platform just chokes on the query anyways. So that&#8217;s not great. But hey, maybe we can do better?</p><h2>What if we just asked them</h2><p>I&#8217;m going back to a point I made earlier in the post: your application is the <em>only thing</em> that knows when it&#8217;s safe to scale down, and it&#8217;s also the <em>only thing</em> that knows if it&#8217;s throwing an error or not. So what if we tied those two things together? What if we made a communication channel to your pods that tracks &#8220;disruption safety&#8221;? What if we &#8220;just asked&#8221;?</p><p>Then we could actually do two things at once: we could have a single built-in metric to track the number of pods that get disrupted outside their &#8220;disruption safety&#8221; window, <em>and</em> we could have a good autoscaling flow:</p><ol><li><p>Autoscaler: hey I need to scale some stuff down, those pods looks good. Hey pods, can I scale you down?</p></li><li><p>Pod 1: Sure, go ahead!</p></li><li><p>Pod 2: Uhhh I&#8217;ve got some cleanup I need to do, check back in five minutes.</p></li><li><p>Pod 3: Nope, I&#8217;m never going away, I am immortal! &#128520;&#128520;&#128520;</p></li><li><p>Autoscaler: OK, I&#8217;m gonna shut Pod 1 down now. Pod 2, thanks for getting back to me, I&#8217;ll try again in five minutes. Pod 3, you are not immortal, you&#8217;ve got 10 minutes before I pull the plug. Make the best of it.</p></li></ol><p>I&#8217;m going to spend a <em>lot</em> more time on this in my rant about pod disruption budgets next week, but unfortunately something like this just isn&#8217;t possible today. The best we can do are hacks like Karpenter&#8217;s <a href="https://karpenter.sh/docs/concepts/disruption/#pod-level-controls">do-not-disrupt</a> annotation or messing around with <a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#pod-deletion-cost">pod deletion cost</a> values.</p><p>But here&#8217;s the thing: there&#8217;s already <em>some</em> precedent for Kubernetes communicating with pods: we have <a href="https://kubernetes.io/docs/concepts/workloads/pods/probes/">three different probes</a> to monitor one half of a pod&#8217;s lifecycle, and while it&#8217;s frequently confusing what the difference is between startup probes, readiness checks, and liveness checks, doesn&#8217;t it seem a bit unbalanced that we don&#8217;t have <em>any</em> probes for the scaledown side of things? It wouldn&#8217;t be <em>that</em> hard to create a <code>disruptionReadiness</code> probe that various other Kubernetes components could query to coordinate scaledown<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><h2>Did u just write a KEP?</h2><p>Ehhhhh, the thing is, is that it actually <em>would</em> be pretty hard to create that probe. Not because the code would be that complicated, but because the process of getting <em>any</em> changes made to the core Pod spec is incredibly burdensome these days<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>. But sure; I guess if any of the Kubernetes authors are reading this and want to write a <a href="https://www.kubernetes.dev/resources/keps/">KEP</a> for this, I wouldn&#8217;t complain. I do think I&#8217;d be remiss to mention how most organizations handle scaling down <em>today</em>, though. There are three steps:</p><ol><li><p>Add middleware that does retries.</p></li><li><p>Set an exorbitantly-large value for <code>terminationGracePeriodSeconds</code>.</p></li><li><p>If you&#8217;re feeling extremely up to the challenge, add a <code>preStop</code><a href="https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/"> hook</a> to gracefully stop doing work and shut down the application before the pod is terminated.</p></li></ol><p>Though to be honest, most people stop at step 2, because step 3 is hard and people are lazy. And so, given that we&#8217;re supposed to be talking about metrics for autoscaling, here are two that you can track to monitor your scale-down performance: first, track your error rates. You probably don&#8217;t even have to do any sort of fancy PromQL envelope function, if you just overlay your error rate metric with your replica count on Grafana, it&#8217;ll be pretty clear if you&#8217;re getting a burst of errors due to scaledown. And second, maybe track how long your pods are sitting in a &#8220;waiting for deletion&#8221; state because you&#8217;ve set your termination grace period seconds to 400 years.</p><p>As always, thanks for reading, and if you do get a job based on my hot interview tips, remember you owe me 10% of your signing bonus.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Fun fact, my goto interview question for a long time was &#8220;design an orchestration platform like Kubernetes that can handle stateful workloads gracefully.&#8221; The number of engineers who just told me flat-out &#8220;that&#8217;s impossible&#8221; and refused to engage further was&#8230; higher than you might expect.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>&#8220;Asking the application&#8221; will become a recurring theme throughout the remainder of this post, and also next week&#8217;s post, so maybe get your shot glass ready.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>You didn&#8217;t know that you were going to be getting free software engineering interviewing tips alongside this post, did you? I can pretty much guarantee that if you follow this script exactly, you&#8217;ll be drowning in offers.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>There was <a href="https://refactoringenglish.com/blog/anti-patterns-software-blogging/">another post making the Internet rounds this week</a> talking about how long, meandering introductions are bad for technical blogs, and so I took that as a personal challenge.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Surely you&#8217;ve been reading this blog long enough not to fall for that. And if you haven&#8217;t, maybe you should <a href="https://blog.appliedcomputing.io/subscribe">subscribe</a>!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Not coincidentally, a disruption readiness probe would also be a <em>great</em> feature to have for that &#8220;stateful version of Kubernetes&#8221; I mentioned in footnote 1. It&#8217;s amazing how many people I interviewed who weren&#8217;t able to get there.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>And for good reason: Pods are extremely core to how Kubernetes operates, so making any changes to them need to be very carefully considered. This is also why we&#8217;ve seen such a proliferation of custom resources and controllers get layered on top of pods in the last five years or so, even within &#8220;core Kubernetes&#8221;&#8212;because it&#8217;s easier to add another layer than it is to make the change where it makes the most sense.  Also probably because nobody&#8217;s going to get promoted for making a change to the Pod spec.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Key Autoscaling Metric #2 - Mean Time to Stop Sucking]]></title><description><![CDATA[Welcome to the second deep dive in our &#8220;Five Key Metrics for Kubernetes Autoscaling&#8221; series!]]></description><link>https://blog.appliedcomputing.io/p/key-autoscaling-metric-2-mean-time</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/key-autoscaling-metric-2-mean-time</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Mon, 05 Oct 2026 22:01:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3j1M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3j1M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3j1M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 424w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 848w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 1272w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3j1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png" width="900" height="734" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:734,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130873,&quot;alt&quot;:&quot;a picture of a purple octopus holding a sign that says \&quot;i don't suck and so can you\&quot;]&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/219004749?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="a picture of a purple octopus holding a sign that says &quot;i don't suck and so can you&quot;]" title="a picture of a purple octopus holding a sign that says &quot;i don't suck and so can you&quot;]" srcset="https://substackcdn.com/image/fetch/$s_!3j1M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 424w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 848w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 1272w, https://substackcdn.com/image/fetch/$s_!3j1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ef9cb71-0024-4920-9eb7-d2a22af74c4d_900x734.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Mr. Squidler's firing on all cylinders today.</figcaption></figure></div><p>Welcome to the second deep dive in our &#8220;<a href="https://blog.appliedcomputing.io/p/five-key-autoscaling-metrics-for">Five Key Metrics for Kubernetes Autoscaling</a>&#8221; series! In last <a href="https://blog.appliedcomputing.io/p/key-autoscaling-metric-1-workload">week&#8217;s post</a>, Ian broke down the node workload committed capacity metric: how this is a useful proxy for &#8220;wasted money&#8221; and why nobody is actually computing this correctly. In <em>this</em> week&#8217;s post, we&#8217;re going to do the same thing, but on the flip side of the cost-reliability coin. Specifically, how can your autoscaling behaviour impact the actual end-user experience for your application?</p><p>We&#8217;re going to do this via a metric that nobody&#8217;s ever heard of before, called &#8220;Mean Time to Stop Sucking&#8221; (MTSS). Just like in last week&#8217;s post, I&#8217;m going to argue that, despite the name, this is a <em>real</em> metric that you <em>ought</em> to be tracking in your autoscaler, and then explain why nobody does.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want to read about more poorly-named metrics that contain a good technical idea buried somewhere deep inside?  Subscribe now!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>But first, let&#8217;s revisit the &#8220;cost-reliability&#8221; coin, since it&#8217;s been a minute<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a><sup> </sup>since we&#8217;ve talked about it.</p><h2>Heads, it&#8217;s cheap. Tails, it&#8217;s not broken.</h2><p>Like I mentioned in the intro post, there are two reasons why you might be interested in autoscaling: cost, and capacity planning. The first reason is the one that everybody talks about, but in my favorite thought experiment of all time, I can make your infrastructure <em>extremely</em> cheap by turning it all off. Or, I can make your infrastructure extremely robust and reliable by provisioning a <a href="https://aws.amazon.com/ec2/instance-types/c7i/">c7i.48xlarge</a> for every pod in your cluster, but I don&#8217;t think your CFO will like that very much. Ideally you&#8217;d like your infrastructure to be &#8220;somewhat&#8221; cheap, but also &#8220;somewhat&#8221; reliable, but how do you know where you are on that curve?</p><p>Ian&#8217;s post last week covered the &#8220;cheap&#8221; side of things; this week and next week we&#8217;ll cover the &#8220;reliability&#8221; aspect. I&#8217;m actually going to break &#8220;reliability&#8221; into two sub-components, that is, &#8220;scaling up&#8221; and &#8220;scaling down&#8221;, because they&#8217;re both important and you need to measure different things for them. On scale up, you need to know if your users&#8217; experience is degraded (and for how long) because it&#8217;s taking you too long to provision new capacity. On scale down, you need to know if your users&#8217; experience is degraded because they were actively doing work and your autoscaler rudely interrupted them.</p><p>So there you have it: autoscaling metrics two and three! We&#8217;re all done here, you can go home now.</p><h2>&#8220;Mean Time to Thing&#8221; metrics considered <s>harmful</s> awesome</h2><p>Just kidding. Let&#8217;s talk more about scaling up.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rGvi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 424w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 848w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 1272w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rGvi!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png" width="1200" height="203.88841927303466" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:201,&quot;width&quot;:1183,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:71558,&quot;alt&quot;:&quot;A bar chart showing the breakdown of steps in scaling up your application; the labels are \&quot;pending pods created\&quot;, \&quot;node provisioning\&quot;, \&quot;node configuration management\&quot;, \&quot;kubernetes scheduling loop\&quot;, \&quot;pod startup\&quot;, and \&quot;application readiness\&quot;]&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/219004749?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="A bar chart showing the breakdown of steps in scaling up your application; the labels are &quot;pending pods created&quot;, &quot;node provisioning&quot;, &quot;node configuration management&quot;, &quot;kubernetes scheduling loop&quot;, &quot;pod startup&quot;, and &quot;application readiness&quot;]" title="A bar chart showing the breakdown of steps in scaling up your application; the labels are &quot;pending pods created&quot;, &quot;node provisioning&quot;, &quot;node configuration management&quot;, &quot;kubernetes scheduling loop&quot;, &quot;pod startup&quot;, and &quot;application readiness&quot;]" srcset="https://substackcdn.com/image/fetch/$s_!rGvi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 424w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 848w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 1272w, https://substackcdn.com/image/fetch/$s_!rGvi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe18a0208-e629-4dfe-9aea-d4221476c335_1183x201.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div><figcaption class="image-caption">All the steps your infrastructure needs to take before your user can see a cat picture.</figcaption></figure></div><p>In Kubernetes, this is how it works: your vibe-coded project gets posted to the orange site and all of a sudden five million AI bros are hitting your homepage. Unfortunately, you only have two pods backing that homepage; so those pods&#8217; CPU utilization goes through the roof, but fortunately you configured the Horizontal Pod Autoscaler (HPA) to watch for overloaded pods, so after sending back a few million 503s, a bunch of new Kubernetes pods get created. Unfortunately, you&#8217;re still on the AWS free tier, so you&#8217;ve only got a single <code>t3.medium</code> instance, and it&#8217;s all full! However, you <em>did</em> actually set up Karpenter to scale up more nodes, so it sees all these pending pods and requests some GPU instances from AWS, because your vibe-coded app needs GPU instances to display your home page for some reason. Sadly, Anthropic has sucked up all the GPU instances so it takes 10 minutes to get some, but this is a fine and normal experience these days. ANYWAYS, once you&#8217;ve finally gotten your compute hardware, your pods can get scheduled on them&#8212;just kidding, actually you have to install 10 years of security updates before Kubelet will start. NOW your pods can start!</p><p>You&#8217;re done, right? Your users can now click buttons on your vibe-coded home page? Ha! Hahahaha! Nope. Sadly, Claude Code wrote your app in Java, which means it takes another five minutes to load your entire database into memory, and do all of the JIT compilation things, and then <em>finally</em> the AI bros can see a vibe-coded picture of a cat.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RYH0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RYH0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 424w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 848w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 1272w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RYH0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png" width="1254" height="1254" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce043081-db4b-46ec-960d-40796a33f613_1254x1254.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1254,&quot;width&quot;:1254,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1935631,&quot;alt&quot;:&quot;An extremely poorly drawn picture of a cat; the colors are horrendous and none of the strokes are within the lines&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/219004749?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="An extremely poorly drawn picture of a cat; the colors are horrendous and none of the strokes are within the lines" title="An extremely poorly drawn picture of a cat; the colors are horrendous and none of the strokes are within the lines" srcset="https://substackcdn.com/image/fetch/$s_!RYH0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 424w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 848w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 1272w, https://substackcdn.com/image/fetch/$s_!RYH0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce043081-db4b-46ec-960d-40796a33f613_1254x1254.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I kid you not, this is the image that ChatGPT made for me when I asked for a cat. I actually had to refine it a couple of times.</figcaption></figure></div><p>OK, so this situation probably isn&#8217;t great, but how can you make it better? Well, the first step is, you need a metric to game! In most organizations that I&#8217;ve worked at, the metric folks look at is &#8220;time from pending pod to scheduled pod&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. Even <em>this</em> metric is somewhat challenging to track, but Karpenter, at least, has an alpha timeseries called <code>karpenter_pods_unbound_time_seconds</code>, which tracks the time from pod creation to pod binding.</p><p>But as you can see, this metric <em>really</em> doesn&#8217;t capture the performance of your autoscaling. In fact, mostly what this metric captures is the cloud provider provisioning delay. That&#8217;s, like, a tiny fraction of what your user cares about! So in the post, I&#8217;d like to propose a different metric, called &#8220;Mean Time to Stop Sucking&#8221;: this is the length of time between when a user <em>makes a request</em> and when their user experience stops being utter dogshit<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><h2>WAHHHHHH, that&#8217;s so hard to measure, I don&#8217;t wanna!</h2><p>I&#8217;ve been in a number of these conversations with folks, and whenever I&#8217;ve pitched this as a metric to monitor, most people have said something to the effect of &#8220;That&#8217;s a really hard metric to measure, and also we don&#8217;t have control over half the things in there. We should just monitor the bits we have control over.&#8221;</p><p>And, like, excuse me. You&#8217;re <em>distributed systems engineers</em>. Your literal job description is to do hard things. And also: there&#8217;s <em>lots</em> of stuff in life you don&#8217;t have any control over, but you monitor anyways<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. The first step to &#8220;improving things&#8221; is <s>asking Claude to fix your broken everything </s>understanding them. If you don&#8217;t <em>understand</em> where all the delays in your scaling are coming from, how on earth do you meaningfully expect to do anything about them?</p><p>Anyways, maybe that&#8217;s a little bit unfair. This <em>is</em> actually a really hard thing to measure, at least in a useful way; if you just measure MTSS as a single number, you know that things are bad, but you don&#8217;t understand why things are bad. But to understand <em>why</em> things are bad, you need to correlate/connect events from multiple different sources. You need to know when your users make requests, which you probably don&#8217;t have a good way to track unless you&#8217;re running some kind of service mesh. You need to link that time to your horizontal autoscaler response, which is based on a bunch of aggregated metrics data from cAdvisor. Then you need to connect the HPA scaling behaviour to your cluster autoscaler response, and <em>finally</em> you need to understand your application performance on startup. So that&#8217;s like, five different metrics you need to track just to track one single metric<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>!</p><p>The good news is, with various distributed tracing infrastructure, you actually <em>can</em> track all of this stuff and get a good breakdown of your scaling activity. Setting up the distributed tracing infrastructure is left as an exercise for the reader.</p><h2>We have achieved suckitude! Now what???</h2><p>So OK, now you understand a) that your application sucks, and b) <em>why</em> your application sucks. How do you make it suck less? Step one is the hardest step: &#8220;convincing your CEO that having an application that sucks is bad for business.&#8221; But this isn&#8217;t an advice column, so I&#8217;m not going to dig into that one any further. </p><p>What I <em>will</em> talk about is how you, as (purportedly) a member of your platform team, can have a positive impact on your scaling up time. It might feel like you don&#8217;t have many levers to pull, but it turns out there&#8217;s actually quite a lot you can do here! Here are some ideas:</p><ol><li><p><strong>Use a faster node autoscaler</strong>: as I pointed out a long, long time ago, there is <a href="https://blog.appliedcomputing.io/p/using-simkube-10-comparing-kubernetes">a marked difference in scaling speed between the Kubernetes Cluster Autoscaler and Karpenter</a>. There&#8217;s a pretty good chance that &#8220;just&#8221; switching to Karpenter is going to have a noticeable improvement on your cluster scaling speed. You don&#8217;t even have to be on AWS for this, there&#8217;s a <a href="https://github.com/Azure/karpenter-provider-azure">Azure Karpenter Provider</a> that&#8217;s been around for a couple years now, and even a <a href="https://github.com/cloudpilot-ai/karpenter-provider-gcp">GCP Karpenter Provider</a>!</p></li><li><p><strong>Optimize your node provisioning time</strong>: most of the cloud providers are &#8220;pretty fast&#8221; at providing you with hardware when you ask for it<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. And yet, the number of folks I&#8217;ve talked to who are like &#8220;it takes 5+ minutes for a node to become available&#8221; is <em>incredibly</em> high. It turns out that as much as we talk wanting about our compute hardware to be <a href="https://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/">cattle, not pets</a>, actually <em>doing</em> that is, well, pretty hard. So usually when a new node comes up, there&#8217;s a lot of &#8220;stuff&#8221; that needs to be done before it can join your Kubernetes cluster: installing OS security updates, node configuration based on environment (prod vs staging), region (US vs somewhere else), etc. All of that stuff takes time, and if you can make it go faster then you can improve your MTSS. I don&#8217;t have a ton of specific advice here because it likely depends on a lot of specific architectural decisions you&#8217;ve made about your infrastructure, but one thing I&#8217;ve seen be effective for some organizations is to <a href="https://blog.appliedcomputing.io/p/boy-ami-glad-to-see-you">bake as much of your architecture into an AMI as possible</a>.</p></li><li><p><strong>Use a faster horizontal autoscaler</strong>: one of the more common reasons<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a> that folks reach for <a href="https://keda.sh/">KEDA</a> is that you can configure the metrics evaluation loop to be more performant than the vanilla HPA. The more often you evaluate your metrics, the faster you can react to changes in those metric values.</p></li><li><p><strong>Use better horizontal scaling metrics</strong>: in what is the definition of the word &#8220;irony&#8221;, every platform engineer on the planet will tell you that you <a href="https://www.youtube.com/watch?v=gSiGFH4ZnS8">should not scale on pod CPU utilization</a>, and also every platform engineer on the planet scales their pods on CPU utilization. If you scale on something else (requests per second, queue depth, really anything at all is better than CPU utilization), you&#8217;re going to have an earlier signal that load is increasing, which means you&#8217;re going to have an earlier response time, and your application is going to suck less. KEDA is also a great choice if you want to scale on arbitrary non-CPU metrics.</p></li><li><p><strong>Scale your entire dependency chain at once</strong>: this lever is the corollary to #4. A very common pattern is that in your microservice architecture, you have a chain of dependencies, A&#8594;B&#8594;C&#8594;D. Service A starts getting increased load based on user requests, which it eventually solves by scaling up. Service B then starts getting increased load based on requests from service A, so then <em>it</em> starts scaling up. This continues until service D has scaled up, and then&#8212;and <em>only</em> then&#8212;will your users stop seeing errors. The better way to do this is to scale your entire dependency chain at once as soon as service A starts seeing increased load. The hard part here is identifying what your dependency chains are, but there was a <a href="https://www.usenix.org/system/files/osdi20-qiu.pdf">very cool paper</a> published a few years ago that provides some insights on how to do this.</p></li><li><p><strong>Make your application start faster</strong>: I&#8217;m pretty sure that you, as a putative platform engineer, had a visceral reaction when I said this. &#8220;But we don&#8217;t control the application code, bro! Those are the product teams, and we try as hard as possible to never talk to them!&#8221; Well, OK, I think I&#8217;ve identified one problem in your organization already. But also, even if you absolutely can&#8217;t change anything at all about your application code, there&#8217;s still stuff here you can change: how many init containers are you running in your pods? How many sidecars are you running that all need to be healthy before your pod readiness check succeeds<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>? Maybe get rid of some of those.</p></li><li><p><strong>Use a predictive autoscaler</strong>: this is the thing that literally everybody jumps to. &#8220;If we could just <em>predict</em> when our load is going to occur, then we don&#8217;t have to do any of the hard work of actually making our application suck less!&#8221; I think folks really like this approach because it sounds sexy as hell, and also probably because implementing a predictive autoscaler is likely going to get you a promotion. But unfortunately I have bad news and worse news for you: a) you&#8217;re probably never going to get it right, because doing predictive autoscaling is really, really, really hard to do well, and b) even if you <em>do</em> get it right, it&#8217;s probably not going to solve all your problems<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. That&#8217;s not to say that predictive autoscaling is <em>always</em> a bad idea or that it&#8217;s <em>never</em> going to work well, but it wouldn&#8217;t be the first thing I reach for in my toolbox. However, if you <em>do</em> decide to go this route, I recommend paying a vendor instead of trying to implement it yourself: there are a couple vendors who do this really well, and about 67 other vendors who do it extremely poorly, and if you pay me a bunch of money I might tell you which is which.</p></li></ol><p>So anyways, there you have it, ACRL&#8217;s simple seven-step guide to making your application scaling suck less! Tune in next time to find out all our secret strategies for scaling back down once you no longer need less suckage.</p><p>As always, thanks for reading!</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Like, probably, literally a minute, this is all I ever talk about in real life, people come up to me all the time and are like &#8220;David won&#8217;t you shut up about cost and reliability already?&#8221; Probably the only thing I talk about more is <a href="https://blog.appliedcomputing.io/p/thoughts-on-scheduling-and-autoscaling">layers</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>I&#8217;m just kidding, most organizations I&#8217;ve worked at don&#8217;t look at metrics.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>BTW, there&#8217;re a bunch of interesting/related metrics in the frontend space: the &#8220;normal&#8221; one folks use there is <a href="https://web.dev/articles/fcp">&#8220;first contentful paint&#8221;</a>. There are various modules in React/Vue/whatever vibe-coded Javascript-framework-of-the-week you&#8217;re using that provide this metric, but these don&#8217;t work for services that don&#8217;t necessarily &#8220;paint&#8221; things&#8212;and while I&#8217;ve never used them, I&#8217;m reasonably certain that none of these front-end services have any way to measure or attribute delays in your infrastructure scaling.</p><p>There is another closely-related metric called &#8220;Mean Time to First Byte&#8221;, which is the length of time between a user request and the first byte that the application returns, but this also isn&#8217;t quite appropriate here because sometimes your Kubernetes apps aren&#8217;t necessarily &#8220;returning bytes&#8221;.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>The weather app on your phone would like a word.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>And then once you&#8217;ve started tracking all of these metrics, your observability team is going to complain at you incessantly about how expensive it all is, but thems the breaks.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Again, unless you need a GPU, in which case you&#8217;re SOL, say it with me now, &#8220;Thanks, Anthropic&#8221;.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>The other incredibly common reason that folks reach for KEDA is that, up until a few months ago, KEDA was the only open-source pod autoscaler that supported &#8220;scaling to zero&#8221;, which is, when you think about it, is kind of bonkers. Anyways, HPA now supports <a href="https://kubernetes.io/blog/2026/09/02/kubernetes-v1-37-hpa-scale-to-zero-beta/">scale to zero</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>If the answer to these questions is &#8220;under 10&#8221;, congratulations, you&#8217;re already doing better than most everybody else out there.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Well, except for getting you that promotion. It&#8217;s virtually guaranteed to do that, even if the autoscaler never works.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Key Autoscaling Metric #1: Workload Committed Capacity]]></title><description><![CDATA[At ACRL, we get to talk to a lot of clusters&#8230; I mean people.]]></description><link>https://blog.appliedcomputing.io/p/key-autoscaling-metric-1-workload</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/key-autoscaling-metric-1-workload</guid><dc:creator><![CDATA[Ian O’Gorman]]></dc:creator><pubDate>Tue, 29 Sep 2026 00:01:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!O2OA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!O2OA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O2OA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O2OA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png" width="900" height="900" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:900,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126301,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/217920213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!O2OA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!O2OA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cb0f1d2-4246-450c-8dc7-3086515659ca_900x900.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Remembering a little fun we had at KubeCon 2025 ATL <a href="https://youtube.com/shorts/ZDlQzhAl8zI">video here</a>.</figcaption></figure></div><p>At ACRL, we get to talk to a lot of clusters&#8230; I mean people. Many of those people manage clusters at scale<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, which we don&#8217;t recommend, but we all have our burdens. In conversations with these individuals, who will remain nameless to protect the guilty, we often hear something like, &#8220;at made-up-for-this-fake-example Corp., we&#8217;re only utilizing 67% of our capacity&#8221;.</p><p>And to that I want to say: &#8220;What do you even mean, bro?&#8221;</p><p>I&#8217;m a bit of a student of economics, by which I mean I lost a lot of money in crypto scams and now I have to do this to survive. But I once had an economics professor who would respond whenever you mentioned a rate like productivity, efficiency, or utilization with a simple question: &#8220;Of what?&#8221; This was a polite way of saying: &#8220;Tell me the damn numerator and denominator, and do it right now.&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>Specificity matters, and at scale, it matters a hell of a lot. Today, let&#8217;s give ourselves permission to be a bit pedantic, because we can&#8217;t afford not to.</p><h2>A cluster (under)utilized</h2><p>In <a href="https://substack.com/home/post/p-216813648">the lead-in to this series</a>, <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;6ca0dcac-5e2a-443f-9ec2-20c412ab4c75&quot;}" data-component-name="MentionToDOM"></span> teased the five key metrics for evaluating autoscalers. We&#8217;re starting off with committed capacity percentage: a cost-oriented metric that reveals how much of our provisioned capacity we&#8217;ve committed to workloads.</p><p>&#8220;Capacity Utilization&#8221; sounds like a straightforward efficiency metric. Unfortunately, as an industry, we are pretty abstract about what utilization is and how we measure it. If you Google cluster utilization, you will get a terrible AI overview, followed by an ad, followed by a fistful of different definitions, including some of the following:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RdS3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RdS3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 424w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 848w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 1272w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RdS3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png" width="736" height="487" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da8b77a4-826d-4d47-b567-a1f947642c55_736x487.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:487,&quot;width&quot;:736,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:67404,&quot;alt&quot;:&quot;Table comparing four capacity metrics: committed capacity is requests divided by provisioned capacity; scheduler saturation is requests divided by allocatable capacity; allocatable utilization is usage divided by allocatable capacity; and request efficiency is usage divided by requests.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/217920213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Table comparing four capacity metrics: committed capacity is requests divided by provisioned capacity; scheduler saturation is requests divided by allocatable capacity; allocatable utilization is usage divided by allocatable capacity; and request efficiency is usage divided by requests." title="Table comparing four capacity metrics: committed capacity is requests divided by provisioned capacity; scheduler saturation is requests divided by allocatable capacity; allocatable utilization is usage divided by allocatable capacity; and request efficiency is usage divided by requests." srcset="https://substackcdn.com/image/fetch/$s_!RdS3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 424w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 848w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 1272w, https://substackcdn.com/image/fetch/$s_!RdS3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda8b77a4-826d-4d47-b567-a1f947642c55_736x487.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">An incredibly ugly table because Substack does not support native tables.</figcaption></figure></div><p>These are all perfectly valid calculations, but they all measure different things. When someone says, &#8220;Our utilization is 57.62%,&#8221; I still have to ask &#8220;Of what?&#8221;</p><p>For our five metrics for evaluating autoscalers, our interest in utilization is as an inverse proxy for waste. For a comparable workload, a higher utilization metric should indicate we are provisioning less excess capacity. It does not prove that those uncommitted resources are removable or that all the requests are necessary. We&#8217;ll get to that in a moment.</p><h2>Show me the money</h2><p>I&#8217;m going to spoil this right out of the gate. If we had to choose just one metric for the cost component of our five metrics for evaluating autoscalers, it would be Committed Capacity Percentage. Well&#8230; sort of.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;    \\text{Committed Capacity Percentage}\n    =\n    \\frac{\\text{R}}\n         {\\text{C}}\n    \\times 100&quot;,&quot;id&quot;:&quot;KTAZRQJWZX&quot;}" data-component-name="LatexBlockToDOM"></div><p>Where:</p><ul><li><p>R = Total scheduled pod requests</p></li><li><p>C = Total provisioned node capacity</p></li></ul><p><br>Typically, committed capacity is calculated separately for CPU and memory. Here, &#8220;requests&#8221; means the resource requests of non-terminated pods on the nodes we are measuring. Unscheduled pods represent unmet demand rather than commitments against the current nodes and should be tracked separately.</p><p>Why this ratio? We love money, dude<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>We&#8217;re paying somebody good money to provision nodes. Any capacity above what is required to support our workload is a cost worth investigating. The numerator tells us how much capacity workloads have claimed. Our denominator shows the full provisioned node capacity.</p><h2>Waste REVEAL thyself</h2><p>Before we start hunting down waste, we need to understand where that capacity went. Total provisioned capacity is not all available to pods. First, we have to reduce our provisioned capacity by host reservations and eviction headroom, giving us our allocatable capacity. Pod requests can claim up to all the remaining allocatable capacity.</p><p>It&#8217;s important to note that <code>kubeReserved</code> and <code>systemReserved</code> belong to a different class of reservation and are not included in pod requests (our numerator). Requests from DaemonSets, however, are pod requests and are included in the numerator. If we were to use allocatable capacity as the divisor, we would lose visibility of the overhead inherent in <code>kubeReserved</code> and <code>systemReserved</code>. Some of this overhead is 100% necessary.</p><p>In most cases, the capacity of a node has two primary dimensions: CPU and memory<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. How the scheduler fits workloads onto nodes is called bin packing and, since we have two dimensions, we actually have a 2D bin packing problem<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. Bin packing is a fascinating, complex domain; however, it is quite easy to visualize potential excess capacity with a conceptual diagram. To illustrate the complexity of this problem see the following diagram: efficiently bin packing a bunch of squares of equal size is quite easy, but inefficiencies emerge quickly with real workloads: we&#8217;ve got long, short, big, small, squares, and rectangles creating many inefficiencies in the packing of this node<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2OF9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2OF9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 424w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 848w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 1272w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2OF9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png" width="1456" height="982" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:982,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130468,&quot;alt&quot;:&quot;Two dimensional conceptual node diagram with CPU and memory for axes. Showing differently sized workload requests,   DaemonSet requests, system reservations, and uncommitted capacity.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/217920213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two dimensional conceptual node diagram with CPU and memory for axes. Showing differently sized workload requests,   DaemonSet requests, system reservations, and uncommitted capacity." title="Two dimensional conceptual node diagram with CPU and memory for axes. Showing differently sized workload requests,   DaemonSet requests, system reservations, and uncommitted capacity." srcset="https://substackcdn.com/image/fetch/$s_!2OF9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 424w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 848w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 1272w, https://substackcdn.com/image/fetch/$s_!2OF9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe94a64fb-bdad-4aa9-882b-67398a1b5aad_1480x998.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Learn how to make this graph and many more at drmorr&#8217;s talk <a href="https://osacon.io/sessions/2026/10-useful-dashboards-you-cant-make-with-grafana/">10 Useful Dashboard You Can&#8217;t Make in Grafana</a> at OSAcon this November!</figcaption></figure></div><p>Bin packing is just one place inefficiencies appear. Other sources of excess capacity may include slow or inefficient scale-down, node/pod shape misalignment, and stranded resources due to scheduling constraints. When these cause additional resources to be provisioned for the same requests, committed capacity falls. Committed capacity percentage can show us that one or more of these symptoms are present, but it will not identify the cause.</p><p>We also can&#8217;t tell from this metric alone if the excess capacity is waste. Some headroom is necessary for a well-functioning cluster; it aids in faster scheduling and overall resilience. That&#8217;s why the goal is not 100% committed capacity percentage. An organization might have a lower provisioned capacity because of a valid reliability concern, which is also why this is just one of our five metrics. That all sounds great, right?</p><p>Wait a second. Some of our allocatable capacity is claimed by DaemonSet replicas on each of our nodes. What if we increased our node count while keeping total provisioned CPU and memory unchanged? In this scenario, our DaemonSet replica count would rise and committed capacity percentage would increase. That seems like the opposite of what we want in a metric for evaluating autoscalers. We would prefer a measure that decreases as overhead increases<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>. Additionally, our output metric (the numerator) should be closely aligned with the actual application workloads; after all, the platform is designed to serve customers, not the other way around<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>!</p><p>With that said, we are pleased to announce, Workload Commitment Percentage, a modified committed capacity metric which addresses this loophole and is more closely aligned with the actual business objective of your clusters.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;    \\text{Workload Commitment Percentage}\n    =\n    \\frac{\\text{R} - \\text{D}}\n         {\\text{C}}\n    \\times 100&quot;,&quot;id&quot;:&quot;KEIQOMOLNN&quot;}" data-component-name="LatexBlockToDOM"></div><p>Where:</p><ul><li><p>R = Total scheduled pod requests</p></li><li><p>D = DaemonSet pod requests</p></li><li><p>C = Total provisioned node capacity</p></li></ul><p>By keeping the denominator as our total provisioned capacity, we still have the broad cost basis for our metric. We exclude DaemonSets from the numerator so as DaemonSet overhead rises our Workload Commitment will fall.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N63K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N63K!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 424w, https://substackcdn.com/image/fetch/$s_!N63K!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 848w, https://substackcdn.com/image/fetch/$s_!N63K!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 1272w, https://substackcdn.com/image/fetch/$s_!N63K!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N63K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png" width="1456" height="1163" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1163,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:193937,&quot;alt&quot;:&quot;Bar chart showing provisioned capacity, allocatable capacity, total pod requests, non-DaemonSet workload requests,   and usage. Committed capacity includes DaemonSet requests; workload commitment excludes them. Both use provisioned   capacity as the denominator.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/217920213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Bar chart showing provisioned capacity, allocatable capacity, total pod requests, non-DaemonSet workload requests,   and usage. Committed capacity includes DaemonSet requests; workload commitment excludes them. Both use provisioned   capacity as the denominator." title="Bar chart showing provisioned capacity, allocatable capacity, total pod requests, non-DaemonSet workload requests,   and usage. Committed capacity includes DaemonSet requests; workload commitment excludes them. Both use provisioned   capacity as the denominator." srcset="https://substackcdn.com/image/fetch/$s_!N63K!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 424w, https://substackcdn.com/image/fetch/$s_!N63K!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 848w, https://substackcdn.com/image/fetch/$s_!N63K!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 1272w, https://substackcdn.com/image/fetch/$s_!N63K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1a1a01-79b0-42da-b483-cb934d272ac9_1491x1191.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">&#8220;utilization&#8221; is between ~44 and ~72%. Feel free to choose whichever one gets you a raise.</figcaption></figure></div><h2>PromQL Example</h2><p>Disclaimer: This probably won&#8217;t work at scale or maybe at all. But hey, knock yourself out.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">avg(sum(kube_pod_status_phase{phase="Running"}
  * on (pod) max by (pod) (kube_pod_container_resource_requests{resource="cpu"})
  * on (pod) max by (pod) (kube_pod_owner{owner_kind!="DaemonSet"})
  * on (pod) group_right max by (pod, node) (kube_pod_info)
) by (node) / on (node)
sum(kube_node_status_capacity{resource="cpu"}) by (node))</code></pre></div><h2>Point of order: Ian, are you stupid?</h2><p>If you talk to your platform teams, nobody actually tracks this metric, and why not? Is it because we&#8217;re stupid? No, it&#8217;s because it&#8217;s hard to do. Here&#8217;s a quick comparison of the other metrics that are easier to track, and why we recommend doing the hard thing anyways.</p><h3>Why not requests / allocatable?</h3><p>What we are calling Scheduler Saturation helps us understand how much of the schedulable budget is claimed. For our cost-oriented comparison, we want the full provisioned resource budget in the denominator. Necessary host reservations still occupy capacity on the nodes we are paying for.</p><h3>Why not usage / requests?</h3><p>Request efficiency helps us determine how closely requests match observed consumption. It can expose oversized requests, which are worth investigating. But it can&#8217;t see an empty node. Add an idle node without changing the pods, their requests, or their usage, and this ratio stays exactly the same, but our bill won&#8217;t. Additionally, requests are not limits, so usage over 100% of requests is possible and a value below 100% isn&#8217;t necessarily waste.</p><h3>Why not usage / allocatable?</h3><p>Allocatable Utilization tells us how busy the capacity available to pods is. This is a helpful metric for investigating resource pressure.</p><p>For evaluating node autoscaling, this leaves something to be desired. The denominator is only our allocatable capacity, which is not what we actually pay for. A useful metric for answering a different question.</p><h3>Why not usage / provisioned?</h3><p>Measuring usage against the full provisioned budget can help evaluate our overall infrastructure efficiency. We choose requests because we&#8217;re interested in how efficiently the autoscaler accommodates the resource demands of the cluster. Whether that demand is appropriately sized is another question that is absolutely worth asking.</p><h2>Let me break this for you</h2><p>Great, we have a metric. Let&#8217;s game it.</p><p>If we simply traversed our cluster and increased the requests on our application pods, we could improve our Workload Commitment without doing anything useful like, I dunno, reducing total cloud costs.</p><p>Larger requests also reduce the placement options for pods and make node consolidation harder for node autoscalers. If the changes produce more nodes, costs could increase.</p><p>There are other ways this percentage can mislead us:</p><ul><li><p>A cluster-wide average can hide resources stranded on specific nodes. You can&#8217;t take CPU from one node and supply it to a pod on another node.</p></li><li><p>An autoscaler could leave unsatisfied demand and earn a good score by keeping its existing nodes full. So we would see a high workload commitment percentage but many pods awaiting capacity<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p></li><li><p>A higher percentage on expensive nodes could cost more than a lower percentage on cheaper nodes. CPU and memory percentages are not denominated in dollars.</p></li></ul><p>For autoscalers to reduce spend, they must make or induce changes at the node level: fewer nodes, cheaper instance types, or both. Other pricing changes can reduce spending too, but workload commitment doesn&#8217;t measure them.</p><p>Use this metric to compare provisioning under comparable workload and reliability requirements. Don&#8217;t treat it as proof of savings or a ranking system for otherwise unrelated clusters.</p><h2>Why track it anyway?</h2><p>Maybe you are thinking, &#8220;Well, Workload Commitment is imperfect, maybe we shouldn&#8217;t worry about tracking it at all.&#8221; However, this is a useful signal that is cheap(-ish) to calculate. You can get all the request data easily from Kubernetes&#8217; native metric sources. Workload Commitment is a great frontline metric and provides a good indicator of avoidable cluster costs<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>.</p><h2>Conclusion</h2><p>Workload Commitment Percentage is our first look at the cost side of evaluating autoscalers. It tells us how much provisioned capacity has been claimed through requests for non-DaemonSet workloads. It can&#8217;t tell us if the requested amounts are sensible, if excess capacity is justified, or whether our applications received the capacity they needed quickly enough. That&#8217;s why we have four more metrics on the way.</p><p>Thanks for reading the ACRL blog. Be sure to subscribe to catch the rest of the series!</p><p>Cheers,</p><p>Ian</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Applied Computing Research Labs is a reader-supported publication. Please subscribe. &#10084;&#65039;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Sometimes the clusters also manage people at scale </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>An early draft of this article included a graph with unlabeled axes. This professor is now rolling over in his grave. RIP to a legend. </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Seriously, please subscribe. We&#8217;re running out of mochas over here. </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>We&#8217;re ignoring resources such as GPUs, storage space, network bandwidth, etc. since these are more specialized and if you&#8217;re using them in your autoscaling we assume you already know what you&#8217;re doing. </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>For a bit more on bin packing check out <a href="https://blog.appliedcomputing.io/p/astronomer-saving-megabux-with-sql">this post</a>. </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Before you flip your lid, Reddit. This is a CONCEPTUAL diagram for a SIMPLIFIED example.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>Not all DaemonSets are platform overhead. </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>You have those&#8230; right? Can you share some with us? </p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>TIL</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>Assuming your observability team isn&#8217;t dropping metrics all willy-nilly to keep costs down. </p></div></div>]]></content:encoded></item><item><title><![CDATA[Five key autoscaling metrics for Kubernetes]]></title><description><![CDATA[Editor&#8217;s note, because APPARENTLY this is required in this, the year of our lord 2026: every word of this post (and every other post on this blog) was generated by my tiny human meat brain.]]></description><link>https://blog.appliedcomputing.io/p/five-key-autoscaling-metrics-for</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/five-key-autoscaling-metrics-for</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Tue, 22 Sep 2026 18:01:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TxR2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TxR2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TxR2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TxR2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png" width="900" height="900" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3508628e-b64e-4661-947b-cde29bbd4535_900x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:900,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:144830,&quot;alt&quot;:&quot;a purple octopus holding a magnifying glass in one tentacle up to his eye; he's holding a sign that reads \&quot;mr. squidler's school 4 kids who can't autoscale gud\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/216813648?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="a purple octopus holding a magnifying glass in one tentacle up to his eye; he's holding a sign that reads &quot;mr. squidler's school 4 kids who can't autoscale gud&quot;" title="a purple octopus holding a magnifying glass in one tentacle up to his eye; he's holding a sign that reads &quot;mr. squidler's school 4 kids who can't autoscale gud&quot;" srcset="https://substackcdn.com/image/fetch/$s_!TxR2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 424w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 848w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 1272w, https://substackcdn.com/image/fetch/$s_!TxR2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3508628e-b64e-4661-947b-cde29bbd4535_900x900.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">&#8220;What is this, a cluster for ants??? It&#8217;s gotta be at least&#8230;. 3 times that big!!!&#8221;</figcaption></figure></div><div class="pullquote"><p>Editor&#8217;s note, because APPARENTLY this <a href="https://blog.appliedcomputing.io/p/stop-accusing-people-of-cheating">is required</a> in this, the year of our lord 2026: every word of this post (and every other post on this blog) was generated by my tiny human meat brain.  No LLM slop machine of any variety was involved in the writing, generation, or production of any content herein.</p></div><p>Earlier this year, I had the opportunity to chat with <a href="https://www.usenix.org/conference/srecon24americas/speaker-or-organizer/courtney-nash-void-0">Courtney Nash</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> at SREcon, and one of the things she reminded me is that &#8220;it&#8217;s OK to say that you&#8217;re an expert&#8221;. So, what am I an expert in? Well, for the last decade-plus, I&#8217;ve been working with autoscalers for distributed systems. I built <a href="https://engineeringblog.yelp.com/2019/11/open-source-clusterman.html">Clusterman</a> at Yelp&#8212;originally for Apache Mesos, and then migrated to Kubernetes; I&#8217;ve contributed to both the <a href="https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md">Kubernetes Cluster Autoscaler</a> and <a href="https://karpenter.sh/">Karpenter</a>; and I&#8217;ve helped a lot of companies do autoscaling better. So, I think it&#8217;s safe to say that I&#8217;m something of an autoscaling expert.</p><p>Autoscaling is a bit of a funny topic: it&#8217;s one of those domains that <em>seems</em> like it shouldn&#8217;t be that hard, but once you start digging into it, you realize that there are a <em>lot</em> of subtleties. Do you scale up reactively, or do you try to predict when new resources will be necessary? How long do you keep unused resources around before scaling down? How do you put guardrails around your system so it doesn&#8217;t scale out of control, and then what do you do when your applications run into those guardrails naturally? How do your answers change when you&#8217;re in the middle of an incident or outage, instead of normal operations<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>?</p><p>Thus, I thought it would be interesting over the next several weeks to deep dive into autoscaling: why do we do it, and how do we do it well? At ACRL, we&#8217;ve identified five &#8220;key metrics&#8221; that you should be tracking for your autoscaling engines, and&#8212;spoiler&#8212;they might not be the normal ones you think of. In this post, we&#8217;ll do a quick overview, and then spend future posts digging into some details. If you&#8217;re curious, follow along below!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want to hear more controversial takes about autoscaling?  Subscribe to the &#8220;Five Key Autoscaling Metrics&#8221; series below!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Why do we autoscale?</h2><p>Before we talk about anything else, it&#8217;s important to understand <em>why</em> we autoscale. There are two main reasons: the first, which is the one everybody talks about, is cost. When you were a small baby startup, you had lots of AWS credits and not that much infrastructure, so you just massively overprovisioned everything, because why not? It&#8217;s all free anyways. At some point, however, you ran out of AWS credits and you have a <em>LOT</em> more infrastructure, and your VP of Finance comes to you with a bill. What do you do? Just shove some autoscaling in there! It&#8217;ll be easy! Take your tech lead a couple weeks at most. Massive savings! Problem solved!</p><p>But it turns out to not be that easy; two weeks later, your tech lead has &#8220;shoved some autoscaling in there&#8221; and you&#8217;ve just had your first autoscaling-related outage. Whoops. So then you kick off a months- or years-long effort to balance your cost savings efforts and your reliability efforts. Better <a href="https://blog.appliedcomputing.io/p/okrs-are-bullshit?utm_source=publication-search">write some OKRs about that!</a>.</p><p>The above example highlights something that I&#8217;ve said hundreds of times: cost and reliability are diametrically opposed. You can make your system extremely robust by purchasing all of the largest, most expensive compute that you can get your hands on<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>, but then you&#8217;re gonna get another visit from your VP of Finance. On the flip side, you can make your system extremely cheap by shutting off all your compute, but then you&#8217;re going to get an unpleasant visit from your CEO because they no longer have a company. Obviously these are extreme examples; in reality, you&#8217;re going to be constantly balancing the tension between these two extremes.</p><p>However, there&#8217;s another reason for autoscaling that most people <em>don&#8217;t</em> talk about, that I might argue is even more important. Even if you&#8217;re running at peak capacity all the time, one of these days your little startup is going to run into limits. You&#8217;re a hyperscaling growth company, after all! You double the number of users of your product every other week! Congratulations! You&#8217;ve won capitalism!</p><p>Back in the previous century, you would handle this by going through an extremely rigorous data analysis exercise called &#8220;capacity planning&#8221;. Unlike today, when you can ask AWS for a new machine and get it approximately instantly<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, it would take between 3 months and a year between when you &#8220;place an order for new hardware&#8221; and &#8220;when it shows up in your datacenter&#8221; and then another several weeks or months after that to when it was actually available for use. So you would need to predict, somewhere between 3 months and a year in advance, how many compute resources you would need. And if you got it wrong, welp, good luck.</p><p>Autoscaling makes that easier; it doesn&#8217;t obviate the need to do capacity planning, but it gives you more leeway if and when your predictions don&#8217;t pan out. This isn&#8217;t a cost thing; it&#8217;s a &#8220;long-term viability of your business&#8221; thing. And, at least to me, that&#8217;s an extremely compelling reason as to &#8220;why you should think about autoscaling even when your business is small.&#8221;</p><h2>What things can we autoscale?</h2><p>Conversations around autoscaling tend to be very confusing, because everyone is talking about something slightly different in the process. As an industry, we&#8217;ve landed on three different types of scaling activities, which I&#8217;ll briefly overview here and then make the probably controversial claim that &#8220;they&#8217;re all the same&#8221;.</p><ol><li><p>Node autoscaling: this is what we were studying in <a href="https://blog.appliedcomputing.io/p/using-simkube-10-comparing-kubernetes?utm_source=publication-search">my comparison</a> of the Kubernetes Cluster Autoscaler and Karpenter. How many compute resources do we need at any given time, and can we get those from your cloud provider of choice? In &#8220;most&#8221; organization, node autoscaling is the purview of the platform or infrastructure team(s).</p></li><li><p>Application (horizontal) autoscaling: if node autoscaling is managing your hardware, application-level autoscaling is managing your software. How many copies of your webserver do you need running at a time? How many shards does your database need? Application-level autoscaling is typically handled by the team that owns the application or service; some applications can scale really well (e.g., your stateless webserver), and some do not (scaling out read replicas for a database is generally not &#8220;too hard&#8221;, but scaling out write replicas is typically much more difficult).</p></li><li><p>Vertical autoscaling: also called &#8220;right-sizing&#8221;, vertical autoscaling is a less-frequently-used form of autoscaling<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. If application scaling is answering the question, &#8220;How many copies of my application do I need?&#8221;, vertical scaling is answering the question, &#8220;How many compute resources does a <em>single copy</em> of my application need?&#8221; Vertical scaling is typically also owned by the application owner, but as mentioned, it&#8217;s a less-frequently reached-for tool. It should also be noted that, if implemented poorly, there is a nasty feedback loop between vertical and horizontal scaling, which means that you can&#8217;t use both unless you do it intelligently.</p></li></ol><p>But OK, back to my original claim, which I said was going to be controversial: these three types of autoscaling <em>are all the same thing</em>. Or, to put it slightly more precisely, they&#8217;re all answering the same question: &#8220;How do I ensure that my application has the resources it needs to do whatever it needs to do <em>right now</em>?&#8221; The only reason we&#8217;ve split &#8220;autoscaling&#8221; into these three different buckets is to make it easier to reason about<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><p>In an ideal world, however, we&#8217;d have a single autoscaler that behaves as follows<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>:</p><ol><li><p>Ooops, we can&#8217;t handle the application load right now!</p></li><li><p>Which thing will resolve the situation? Is it: a. Add more hardware b. Add more software c. Make the software bigger d. Some combination of the above e. Some combination of the above while also kicking out some other software that isn&#8217;t needed right now</p></li><li><p>Do the thing you identified in step 2.</p></li></ol><p>If you really wanted to get fancy, you&#8217;d through a machine learning algorithm on top of that and add some predictive capabilities to your autoscaler, but I&#8217;d argue that in 98% of cases, as long as you can provision new resources quickly enough, predictive scaling isn&#8217;t needed.</p><p>But anyways, now that we know <em>why</em> we autoscale and <em>how</em> we can autoscale, let&#8217;s wrap up this post by answering the question, &#8220;How do we know if we&#8217;re doing a good job?&#8221;</p><h2>The five key autoscaling metrics</h2><p>Most organizations look at one metric for their autoscalers: provisioned capacity versus requested capacity. This is, basically, <em>only</em> looking at the cost side of things. The higher your utilization, the fewer resources you need, and the less money you have to pay AWS. But as we&#8217;ve already identified, this is an extremely reductive way of looking at things. Autoscaling is a tool that we are using to find our place on the cost, reliability, and capacity-planning spectrum, and if you&#8217;re not paying attention to metrics that address all three of these areas, then you&#8217;re flying blind. So let&#8217;s briefly talk about ACRL&#8217;s Five Key Metrics for Autoscaling :tm:</p><ol><li><p>Committed capacity percentage - I already described this metric, but just for the sake of putting it in a nice list, your capacity metric measures &#8220;how many resources you&#8217;re wasting&#8221;. Higher committed capacity = less wasted money. Easy. &#8216;Nuff said.</p></li><li><p>Error rates - If capacity commitment is measuring your cost, error rates measure your reliability. When you get a burst of traffic and you have to scale up, do you throw a bunch of errors until the capacity comes online? When you start scaling down, do you interrupt a bunch of in-flight requests and force retries? Or do you do graceful scaling, both <em>up</em> and <em>down</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>?</p></li><li><p>Mean Time to First Byte (MTFB) - this is a concept I borrowed from the front-end community, where they have the concept of &#8220;mean time to content&#8221;: basically, how long does it take from when a user enters a URL in their browser to when they can do something meaningful on the webpage? MTFB is a similar concept: how long does it take from when an autoscaler decides to scale up, to that application or service actually doing useful work?<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. If capacity and error rates are tracking your cost and reliability metrics, MTFB is tracking your &#8220;scaling responsiveness&#8221;.</p></li><li><p>Application disruption - this metric is one of my personal pet peeves, because Kubernetes gives you basically no ability to control for this. But, ideally, your autoscaling engine isn&#8217;t causing a bunch of disruption to running pods that is forcing you to repeat a bunch of work, because that&#8217;s just wasteful. Stay tuned for the longer rant on this one.</p></li><li><p>Application churn - this final metric is not often talked about, but anybody who&#8217;s written an autoscaler (or, heck, any kind of engineering system) has dealt with the problem: your autoscaler thinks you need two and a half replicas, but unfortunately, you can&#8217;t <em>have</em> two and a half replicas, so instead you get two. And then three. And then two. And then three. And then two. And then.... Anyways, go look up <a href="https://en.wikipedia.org/wiki/PID_controller">PID controllers</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>.</p></li></ol><p>So there you have it! Track those metrics, optimize your autoscaler for all five of them at the same time, and boom! You&#8217;re all done!</p><p>Ahahahahah. Haha! HAHA! I can&#8217;t even say that with a straight face<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>. Anyways, we&#8217;re going to take a closer look at each of these metrics over the next several weeks, and we&#8217;ll give some examples from our past experience about why they&#8217;re important and what can happen if you don&#8217;t pay attention to them.</p><p>As always, thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Who is awesome, by the way!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>I can&#8217;t count the number of times I&#8217;ve seen or been involved in an incident where the system loses all its traffic, the autoscaler scales everything down (because there&#8217;s no traffic and thus no reason to keep the resources around!), and then when the traffic comes back, it causes a second &#8220;thundering herd&#8221; outage because there are no longer any resources to handle the load.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>This is Anthropic&#8217;s strategy, but for some reason it doesn&#8217;t seem to be working out for them.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Unless it&#8217;s a GPU-backed node, sigh.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><span>However, with the advent of </span><a href="https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/">in-place pod resizing</a><span> in Kubernetes 1.35, I expect to see a lot more places experimenting with vertical scaling.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><span>If you really want to throw a monkey wrench into things, I also believe that </span><em>scheduling</em><span> is the same thing as well! If this weren&#8217;t true, why does every autoscaler in existence also run a scheduling simulation as a part of its feedback loop?</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><span>Astute readers might notice that this is just &#8220;getting rid of some of the layers&#8221; which is a concept I haven&#8217;t talked about </span><a href="https://blog.appliedcomputing.io/p/conways-law-and-kubernetes?utm_source=publication-search">on the blog</a><span> for a while, but I still think about basically all the time.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>&#8220;But David,&#8221; I hear you say, &#8220;I&#8217;m just a lowly platform engineer, and that stuff is controlled by our service mesh team! I can&#8217;t control how we do retries!&#8221; To which I respond, &#8220;Don&#8217;t make me tap the &#8216;layers&#8217; sign.&#8221;</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><span>Most of the other articles I&#8217;ve read about this look at &#8220;mean time to scheduling&#8221;, e.g., how long between an autoscaler action and a running application? But a running application isn&#8217;t necessarily a </span><em>useful</em><span> application, and we care about useful things here. The objection to this metric is the same as before: we don&#8217;t have control over the application stack! Again: sign. Layers.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>I&#8217;ve been having a lot of conversations recently with folks about how the software engineering community refuses to learn any lessons from any of the other engineering communities that have predated it, and instead just treats every problem as &#8220;100% brand new and nobody else in the world could possibly have any insight or encountered it before&#8221;, and, well, I unfortunately think the &#8220;application churn&#8221; metric is one of these cases.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p><span>Also I really hope you read that in your best </span><a href="https://en.wikipedia.org/wiki/Claptrap">Claptrap</a><span> impersonation, because I sure did.</span></p></div></div>]]></content:encoded></item><item><title><![CDATA[Dear Kubernetes: You need to get your priorities straight]]></title><description><![CDATA[Editor&#8217;s note: I&#8217;ve been informed that the introduction to this post is convoluted and confusing.]]></description><link>https://blog.appliedcomputing.io/p/dear-kubernetes-you-need-to-get-your</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/dear-kubernetes-you-need-to-get-your</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Sat, 12 Sep 2026 02:30:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NBbJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NBbJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NBbJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 424w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 848w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 1272w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NBbJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png" width="1000" height="800" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:800,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:310924,&quot;alt&quot;:&quot;Comic-book style graphic that says \&quot;Dr. Morr says NO!\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/215293275?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Comic-book style graphic that says &quot;Dr. Morr says NO!&quot;" title="Comic-book style graphic that says &quot;Dr. Morr says NO!&quot;" srcset="https://substackcdn.com/image/fetch/$s_!NBbJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 424w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 848w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 1272w, https://substackcdn.com/image/fetch/$s_!NBbJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44ecec3c-7d07-44fd-a621-b9a69424fa13_1000x800.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I spent way too much time making this image, I am extremely proud of it, and I am 100,000,000,000% turning this into a laptop sticker.</figcaption></figure></div><blockquote><p>Editor&#8217;s note: I&#8217;ve been informed that the introduction to this post is convoluted and confusing. If you&#8217;d like, feel free to skip it and go straight to the first subsection.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want to read more rambling, introductions about topics completely unrelated to the rest of the post?  Subscribe below!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>When I was in grad school, I spent a LOT of time thinking about <a href="https://en.wikipedia.org/wiki/Linear_programming#Integer_unknowns">mixed integer programming</a>; in fact, my <a href="https://evokewonder.com/research/docs/dmorrison-2014-thesis.pdf">PhD thesis</a> was primarily focused on developing new techniques for solving (certain classes of) integer programs). In short, if you&#8217;re unfamiliar with this concept, an &#8220;integer program&#8221; is just a system of equations that encodes certain &#8220;state&#8221; as variables that can only take certain (integer) values. The goal is to find the &#8220;best&#8221; set of assignments to those variables so as to maximize some objective or goal.</p><p>See, the thing is, though, integer programming is hard. Like, <a href="https://en.wikipedia.org/wiki/P_versus_NP_problem">really hard</a>. Like, <a href="https://blog.computationalcomplexity.org/2025/10/ai-and-intro-theory.html">as hard as a lot of other really hard problems</a>. And so when you&#8217;re confronted with one of these problems, one of the things you might do is &#8220;try to make it easier&#8221;. For mixed-integer programming, one of the ways you can make things easier is called the <a href="https://en.wikipedia.org/wiki/Big_M_method#Other_usage">&#8220;Big-M Method&#8221;</a>. The idea is, &#8220;Well, OK, it&#8217;s really hard to find integer solutions to the equation <code>x = y</code>, but maybe instead we can replace that equation with <code>x &#8722; y &#8804; Mz</code> and <code>x &#8722; y &#8805; Mz</code> for some other variable <code>z</code> and some extremely large constant <code>M</code>&#8221;.</p><p>The problem with this approach is that <code>M</code> has to be &#8220;big enough&#8221; so that your solver can&#8217;t &#8220;cheat&#8221; and find a solution to these equations where <code>x &#8800; y</code>. BUT, and this is the important piece for the rest of this entire post, which I promise is relevant, the bigger you make <code>M</code>, the harder the problem is to solve in practice<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, <em>specifically</em> because <code>M</code> doesn&#8217;t have any meaning or relationship to the problem you&#8217;re actually trying to solve.  And therein lies the problem with Kubernetes.</p><h2>Weight, weight, don&#8217;t tell me</h2><p>So I&#8217;m pretty sure at least all of you are going &#8220;WTF is drmorr on about today?&#8221; and are ready to ragequit your browser. But if you bear with me<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> I promise I&#8217;ll make it all make sense.</p><p>Let&#8217;s back up. I work on this extremely complex distributed system called &#8220;Kubernetes&#8221;. And because Kubernetes wants to be all things for all people, there are a lot of different ways that any individual user can <em>configure</em> Kubernetes. And in many cases it is impossible<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> for the authors of Kubernetes to know <em>how</em> end users might want to configure Kubernetes; questions like &#8220;is <em>this</em> thing more important than <em>that</em> thing?&#8221; might reasonably have different answers for different people. And whenever we run into one of those questions, instead of doing the hard work of understanding our customers, we just do the distributed-systems equivalent of Big-M and say &#8220;Screw it, we&#8217;ll just slap a priority on that sucker and let the users decide.&#8221;</p><p>Here is a (non-exhaustive) list of where I&#8217;ve encountered this behaviour:</p><ul><li><p>The <a href="https://kubernetes.io/docs/reference/scheduling/config/">kube-scheduler scoring step</a>: kube-scheduler operates on pods one-at-a-time<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, and for each pod it goes through a long pipeline which first filters out nodes that the pod can&#8217;t schedule on, and then scores the remaining nodes. There are a lot of different tests that are run to compute the score of a node: things like &#8220;how many pods are already on the node&#8221;, &#8220;how many resources would be left on the node if the pod gets scheduled there&#8221;, &#8220;are there any &#8216;soft&#8217; affinity or anti-affinity rules in play&#8221;, etc. Now the thing is that each of these tests are weighted, and the weight controls how much the test contributes to the final score. And of course, because different users might have different goals and priorities, these weights are user-configurable.</p></li><li><p>The <a href="https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-are-expanders">Cluster Autoscaler priority expander</a>: Same deal as kube-scheduler, different problem. Cluster Autoscaler needs to pick one of <code>N</code> different node groups to scale up when there are pending pods in the cluster; it first filters out the invalid ones and then selects the node group based on a weighted rank of the remainder, and the weights are chosen by the user.  This one&#8217;s fun because in 98% of cases, it reduces to picking the first value in a sorted list; the <em>values</em> of the weights are completely irrelevant.</p></li><li><p>The <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/">Pod Priority mechanism</a>: Kubernetes allows you to specify priorities on your pods themselves, so that if there isn&#8217;t enough space in your cluster for a pending pod and it&#8217;s a higher priority than another (currently running) pod, the lower-priority pod will get evicted and the higher-priority pod can then take up the newly-freed slot<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. This one&#8217;s particularly fun because setting pod priorities can have unintended knock-on effects to your pod&#8217;s QoS class.</p></li><li><p><a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#pod-deletion-cost">Pod Deletion Cost annotations</a>: If you didn&#8217;t want just <em>one</em> way of setting pod priorities, don&#8217;t worry, here&#8217;s another! You can set the <code>controller.kubernetes.io/pod-deletion-cost</code> annotation on your pods to tell Kubernetes, hey, when my ReplicaSet scales down, delete the pods with lower deletion cost before the ones with higher deletion cost! As a fun bonus, <a href="https://github.com/kubernetes-sigs/karpenter/blob/main/designs/balanced-consolidation.md#separate-disruption-cost-annotation">Karpenter&#8217;s balanced</a> consolidation mode also now relies on this annotation to determine the cost of consolidating a node.</p></li></ul><p>I&#8217;m sure there are others, but these are ones that I&#8217;ve personally interacted with (and been bitten by) in the past. The problem in each of these cases is the same as the Big-M problem in integer programming: these numeric weights a) have no meaning except that which is assigned by the users of the system, b) changing the value of the weights can have unintended consequences in distant parts of the system, and c) the temptation is to make these weight values really, really large to signal &#8220;Hey this thing is REALLY IMPORTANT, don&#8217;t touch it&#8221;, and bigger numbers = bigger problems.</p><p>Let&#8217;s look at a couple of examples.</p><h2>Example 1: kube-scheduler node scoring</h2><p>The first example of how this can bite you is in kube-scheduler; here, the weights are being applied to the different tests that kube-scheduler executes as part of node scoring. Let&#8217;s imagine that there are five different such tests. For our purposes, a test <code>T</code><sub><span>i</span></sub> is a real-valued function that takes some arbitrary inputs about the state of the cluster and the node under consideration; in other words, in math-y lingo, <code>T</code><sub><span>i</span></sub><code>: K&#215;n &#8594; R</code>.</p><p>Each test is then given a user-specified weight <span>wi&#8712;R</span>, and the score for a particular node in the cluster is calculated as the weighted sum of the tests evaluated for that node. Moreover, the selected node is the one that maximizes this weighted sum:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_n w_1 T_1(\\mathcal{K}, n) +w_2 T_2(\\mathcal{K}, n) +w_3 T_3(\\mathcal{K}, n) \\\\\n+w_4 T_4(\\mathcal{K}, n) +w_5 T_5(\\mathcal{K}, n)&quot;,&quot;id&quot;:&quot;OUYNBGSNKY&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now, let&#8217;s suppose as the administrator of the cluster, you want to ensure that pods always get scheduled on cheaper nodes before they get scheduled on more expensive nodes<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. Let&#8217;s say that <span>T</span><sub><span>5</span></sub> returns the cost of the node, so you set <span>w</span><sub><span>5</span></sub> to the largest negative number you can think of, say, -100. Then you go about your day, and all of a sudden you get paged because some pod got scheduled to an expensive node before a cheap node! What! How can this be???</p><p>Well, in this example, you didn&#8217;t care about any of the other tests, so you just left <code>w</code><sub>1</sub><code>,...,w</code><sub>4</sub><code>=1</code>. Each of the other tests <code>T</code><sub>1</sub><code>,...,T</code><sub>4</sub> returns the value 300, and since the expensive node of course <a href="https://www.youtube.com/watch?v=KEjFjpHKi2M&amp;list=PLOgtqKaB5McAOIyl18Gwh7Ks9CUWYxWwQ&amp;index=4">costs $10</a>, the final node score is 200. Turns out that for all the other (cheaper) nodes, the other tests returned 1, so the weighted sum for those nodes was -96.</p><p>&#8220;Well, ok, that can&#8217;t be right,&#8221; I hear you say from all the way over here. &#8220;Lemme just adjust some of those other weights real quick!&#8221; And bam! You have just entered the 12th circle of hell without even realizing it.</p><h2>Example 2: pod deletion cost and balanced consolidation</h2><p>In the previous example, there is a single hypothetical user that is controlling all of the numeric weights. So even though the weights don&#8217;t have any semantic meaning to the system, there is a single user that can assign some semantic meaning to their choices. This second example is more insidious, because it involves multiple users with competing incentives getting to pick the weights.</p><p>In Karpenter 1.14, the <a href="https://karpenter.sh/docs/concepts/disruption/#balanced-consolidation">Balanced Consolidation</a> disruption mode was introduced. The high-level idea here is that Karpenter shouldn&#8217;t disrupt nodes where the benefits don&#8217;t outweigh the drawbacks. Since Karpenter&#8217;s whole shtick is &#8220;save money&#8221;, the authors used &#8220;the amount you would save by disrupting the node&#8221; for the &#8220;benefit&#8221; side of that equation. On the &#8220;drawbacks&#8221; side of the equation is &#8220;how much disruption does this cause&#8221;, and the entire calculation boils down to comparing the &#8220;benefits / disruption&#8221; ratio <a href="https://github.com/kubernetes-sigs/karpenter/blob/main/designs/balanced-consolidation.md#why-k2">to 2</a>.</p><p>The problem is that the &#8220;disruption&#8221; part of this ratio is (essentially) just a weighted sum of the pods on the node, and the <em>owners of the pods</em> (who may or may not be on the same team, and who may or may not have an antagonistic relationship towards each other) get to set those weights, via the <code>pod-deletion-cost</code> annotation. So if engineers on one team see their pods getting frequently disrupted by Karpenter, they&#8217;re going to increase the weight of those pods so that some other team of losers gets <em>their</em> pods disrupted instead. And then that other team is going to play a tit-for-tat game until both teams have set their deletion costs to <code>INT_MAX</code> and are furious with each other, and congratulations, you&#8217;re back in the 12th circle of hell<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>.</p><h2>Who could have foreseen this?</h2><p>I&#8217;d like to argue in this post that the underlying cause of all these problems (including the Big-M problem at the beginning of the post) boils down to two factors:</p><ol><li><p>Allowing (essentially) unbounded range on your weights</p></li><li><p>Using unit-less values for your weights</p></li></ol><p>The first problem is somewhat obvious: allowing big numbers means there is always the temptation to just say &#8220;well THIS thing is really important, so lemme 10x the weight!&#8221; And then later on when the math fails you, you go and 10x some <em>other</em> really important thing, and the cycle continues ad infinitum.</p><p>The second problem I think is more fundamental. You&#8217;re trying to compare <em>fundamentally incomparable things</em> by assigning them numeric weights. You, as the external operator, give those weights some semantic meaning, but math doesn&#8217;t know what that semantic meaning is: all math sees is numbers, and numbers are easy to compare<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>! The &#8220;right&#8221; answer here is &#8220;don&#8217;t compare incomparable things&#8221; but I admit that isn&#8217;t always practical, so my second-place recommendation is to always make sure the things you&#8217;re comparing have units. As we all learned in physics class, unitless numbers are bad.</p><p>For example, in the Karpenter balanced consolidation case, an arguably &#8220;better&#8221; notion of disruption might be something like &#8220;time needed to get all the pods on the node back to <code>Running</code>&#8221;. Then you can consider the ratio of &#8220;dollars saved / time spent rescheduling&#8221;, and that lets you start to semantically-encode some of your external priorities into your policy engine. It might be fine to spend 2 minutes rescheduling all the pods if you&#8217;re going to save $100, but if you&#8217;re only going to save 50 cent, it might not be worth it<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p><p>My last suggestion here is, if you <em>do</em> have to make up some unitless integer weights, consider dramatically constraining the range of values those weights can take on. Nobody can reason about the difference between something with weight 147938964 and 147938965, but people can actually think about the difference between, say, 1 and 3. And as a corollary to this, if you find that you only need a few weight values, maybe see if you can get that number down to two, because then you can replace it with a boolean: I claim that nobody ever needs to set a pod deletion cost in [-2147483648, 2147483647]. All anybody ever <em>actually</em> cares about is &#8220;delete these pods first, then delete those ones&#8221;, which is just two states.</p><p>In closing, just remember, kids: the next time you think about adding in a user-configurable integer weight value to your distributed system (or your integer program), &#8220;Dr. Morr says NO!&#8221;</p><p>As always, thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>And also in part because large values of <code>M</code> start to introduce all kinds of floating-point errors in your calculations.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p><a href="https://strikerless.com/wp-content/uploads/2016/07/bear-with-me.jpg">https://strikerless.com/wp-content/uploads/2016/07/bear-with-me.jpg</a></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Or at least, it&#8217;s really hard and everybody is lazy.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>More-or-less</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p><span>By the way, this is how people typically configure &#8220;warm pools&#8221; in Kubernetes: use something like the </span><a href="https://github.com/kubernetes-sigs/cluster-proportional-autoscaler">cluster proportional autoscaler</a><span> to schedule a bunch of &#8220;do-nothing&#8221; pods with extremely low priority. When higher-priority workloads come in, they can evict the do-nothing pods and start doing work immediately. The do-nothing pods then will trigger your node autoscaler to scale up and refill your warm pool.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><span>This isn&#8217;t a real Kubernetes scheduler priority, but it </span><em>could</em><span> be, and it&#8217;s useful for illustrative purposes.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><span>As a fun aside, you&#8217;ve </span><em>also</em><span> entered the NaN&#8217;th circle of hell, because the Karpenter balanced mode computation does a bunch of numeric divisions, and big numbers are murder on floating point calculations.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>[Citation needed]</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Although he does make some pretty good rap.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Bazel's UX is really, really, really bad]]></title><description><![CDATA[Hi friends!]]></description><link>https://blog.appliedcomputing.io/p/bazels-ux-is-really-really-really</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/bazels-ux-is-really-really-really</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 28 Aug 2026 21:30:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!M8wv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdbcc8-dc09-4e12-8fc3-348cb9c2691e_518x518.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi friends! It has been utter chaos here at ACRL and I haven&#8217;t had a moment to come up for air in several weeks.  However, I think<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> things are calming down a bit on my end, and plus I had something so utterly unpleasant happen to me that I just had to drop everything and write a blog post about it. The name of that unpleasant thing? <em>Bazel</em>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want to read more rants like this one?  Consider becoming a free or paid subscriber!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Huh? What&#8217;s a bazel?</h2><p>So Google has this questionable habit of building something internally, and then releasing a worse version of it as an open-source project, and then throwing a lot of marketing money at the open-source version until it takes over the world. In this post, I&#8217;m not talking about the most famous example of this<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, but instead a lesser-known project called &#8220;bazel&#8221;, which is based off an internal Google project called &#8220;blaze&#8221;.</p><p><a href="https://bazel.build/">Bazel</a><span>, if you&#8217;re lucky enough to have never heard of it before, is a tool for building software. It&#8217;s kinda like </span><a href="https://en.wikipedia.org/wiki/Make_(software)">Make</a><span>, or </span><a href="https://cmake.org/">CMake</a><span>, or </span><a href="https://ninja-build.org/">Ninja</a><span>, or any number of other build systems. The singular task of a build system is: take a bunch of source files written in text, and produce one or more executables at the end. Bazel adds onto this goal, in that it creates &#8220;reproducible builds&#8221;, which means essentially, if I give you the exact same set of inputs to the build system that I used on </span><em>my</em><span> computer, and you build it on </span><em>your</em><span> computer, you will get a bit-for-bit identical binary executable at the end</span><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>There&#8217;s a variety of reasons why you might want that, ranging from software provenance and supply-chain security to caching and build timing, and I&#8217;d just like to acknowledge that Bazel (probably) is solving a really hard problem that some people in the industry need solved. But, and this is my central thesis for this blog post, the <em>user experience as someone who just wants to compile some code and run some tests is <strong>terrible</strong></em>. Here&#8217;s ten reasons why.</p><h2>Reason 1: You have the wrong bazel installed</h2><p>When I first started working with bazel, I was on a Mac, so I did the natural and normal thing that you do on MacOS: <code>brew install bazel</code>. Then, later, I needed to upgrade bazel and was greeted with an arcane message that said I needed to</p><pre><code><code>cd "/opt/homebrew/Cellar/bazel/9.0.0/libexec/bin" &amp;&amp; \
    curl -fLO https://releases.bazel.build/9.1.1/release/bazel-9.1.1-darwin-arm64 &amp;&amp; \
    chmod +x bazel-9.1.1-darwin-arm64 &amp;&amp; \
    cd -</code></code></pre><p>What? What year is this? I thought homebrew was just supposed to handle upgrades for you? Why are we downloading release targets by hand and installing them in homebrew&#8217;s directory tree???</p><p>Anyways, not to spoil the punchline, but apparently the actual tool I needed to install is called <code>bazelisk</code>. Now to be fully transparent: if you <em>read</em> the Bazel docs<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, this is their <a href="https://bazel.build/install">recommended approach</a>, but I&#8217;m placing this complaint at the top of my post because it is emblematic of Bazel&#8217;s apparent disdain for the people who are actually <em>using</em> the software. </p><p>If Bazelisk is the tool that developers are supposed to use, <em>why</em> is there a separate homebrew target for <code>bazel</code>? Why does nothing in the tool itself ever tell you, &#8220;Hey, you might have installed the wrong thing, here&#8217;s the tool you actually want&#8221;? Why is the only message it provides when you try to upgrade &#8220;Download the latest thing from the releases page&#8221; instead of either &#8220;just doing the upgrade&#8221; or, again, <em>telling you that you have the wrong tool installed</em>???</p><p>This is a theme we will return to throughout this blog post: Bazel itself clearly knows that something is wrong, and it clearly should (or at least <em>could</em>) either resolve the problem automatically or tell the user how to resolve it, but instead it does neither of these and expects the user to flounder through extremely arcane and indecipherable error messages. This is not good UX design.</p><h2>Reason 2: Why are there all these BUILD files everywhere?</h2><p>One of the ways that Bazel accomplishes its tasks is by building a definitive list of dependencies for every single package, library, or module for the system it is building. This is fine and necessary to ensure reproducible builds. The way it accomplishes this task is by putting a <code>BUILD</code> file in every single subdirectory of your project. Again, also fine. </p><p>Now, naturally, any time your build changes (by including a new dependency or source file), you have to update the <code>BUILD</code> file, and thankfully, bazel doesn&#8217;t force you to do this by hand. Instead, it provides a tool called <code>gazelle</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a> to update your <code>BUILD</code> files. BUT&#8212;if you, say, happen to forget to run <code>gazelle</code>, Bazel just refuses to compile. It just will complain that &#8220;some symbol is undefined&#8221;.  It doesn&#8217;t try to resolve it automatically (which would be the nicest approach); it also doesn&#8217;t tell you how to resolve it, which is extra confusing if you&#8217;re a newcomer, because clearly the symbol is defined, it&#8217;s right &amp;$*#ing there, I&#8217;m looking at it in my text editor. All Bazel says, in the middle of a stack of other output<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>, is &#8220;symbol is undefined&#8221;.</p><p>Again, Bazel <em>knows</em> that something is wrong. It <em>knows</em> how to fix it. And yet, it does nothing, provides no helpful information to the user, and leaves you to scrounge around for feedback until you&#8217;ve fully internalized the fact that you just need to run <code>gazelle</code> after every single change to any file in the source tree, just in case.</p><p>(A weird aside: if you use various non-code files&#8212;e.g., text files for <a href="https://testthat.r-lib.org/articles/snapshotting.html">snapshot testing</a>, those files <em>also</em> need to be contained in bazel&#8217;s <code>BUILD</code> files, even though they have nothing to do with the generated code. I think this makes sense if you think about it long enough, but it was still weird to me when I first encountered it).</p><h2>Reason 3: What is up with this syntax?</h2><p>When you run any bazel command, you have to specify what you&#8217;re operating on: e.g., <code>bazel build //some/package</code>. The thing following the command &#8220;looks like&#8221; a directory path, except it is not. Sometimes you include the beginning forward double-slash. Sometimes you do not. Sometimes you include a trailing ellipsis (<code>...</code>). Sometimes you do not. Sometimes the path goes after a double-hyphen (presumably to separate this from other command-line arguments, which is a fairly standard convention), sometimes it does not.</p><p>The point here is: interacting with Bazel requires you to learn a new syntax that looks and acts <em>sort of</em> like a directory structure, but is not <em>actually</em> a directory structure, and if you do it wrong, Bazel just complains about missing files (and/or symlinks, which is extra confusing, what symlinks are we even talking about?). It clearly knows that the command is structured incorrectly, but it doesn&#8217;t give you any indication for how to structure things <em>actually correctly</em>, you&#8217;re just left to flounder.</p><h2>Reason 4: How do I even build something successfully?</h2><p>The whole point&#8212;the raison d&#8217;&#234;tre, as it were&#8212;of Bazel is to <em>build things</em>. So then, why is it so incredibly difficult to <em>actually build something resembling an executable with the tool</em>? The first time I tried to build things I had my paths structured wrong and it complained about missing symlinks (see Reason #3, also note that I was just following the command that was listed in the documentation, which, sigh. I get that documentation goes out of date, but come on).</p><p>Once I fixed my paths, it then failed with the following error:</p><pre><code><code>curl: (22) The requested URL returned error: 401
WARNING: Download from https://some-url.com/whatever failed: class java.io.FileNotFoundException GET returned 404 Not Found
ERROR: no such package '@@some_stuff++whatever+blah_blah_blah_linux_amd64//': oauth failed:</code></code></pre><p>(Note, again, this error is buried underneath a wall of other text).</p><p>But OK, clearly it&#8217;s trying to download &#8220;something&#8221; from &#8220;somewhere&#8221; and it&#8217;s <em>either</em> getting a 401 Unauthorized response OR a 404 Not Found response, and it&#8217;s not immediately clear which, but more to the point: this error was indicating that I hadn&#8217;t logged in to a private docker registry that was NOT hosted at <code>some-url.com</code>. In fact, nowhere in the entire error message does it say <em>anything</em> about Docker.</p><p>Also, what on earth is this nonsense?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">@@some_stuff++whatever+blah_blah_blah_linux_amd64//</code></pre></div><p>I have no idea what any of that means, except that it&#8217;s clearly some dependency that it&#8217;s trying to download. Bazel has to know where it&#8217;s trying to download from, and it has to know why it&#8217;s failing, but it doesn&#8217;t actually tell you <em>any</em> of that information, it just gives you an unrelated error message in arcane syntax and leaves you to flounder.</p><p>Anyways, I logged into the private registry and tried to build again, and was greeted with the following error:</p><pre><code><code>ERROR: no such package '@@+http_io+whatever_v5//file` java.io.IOException: Error downloading [...] GET returned 404 Not Found
ERROR: /path/to/BUILD:67:42 //thing/im/building depends on @@+http_io+whatever_v5//file in repository @@+http_io+whatever_v5 which failed to fetch.  no such package</code></code></pre><p>OK again so clearly again it&#8217;s depending on some package that&#8230; doesn&#8217;t exist? This makes no sense. Clearly it exists for other people, or everyone would be complaining that everything is broken. Why doesn&#8217;t it exist for me?</p><p>For quite a while I was stuck at this stage in the process, and resorted to just pushing my code to GitHub and letting CI run the build and the tests, because CI was obviously configured correctly and my system was not, and that was easier than trying to figure out and resolve whatever was broken for me locally. Man it sure would have been nice if Bazel had given me a clue!</p><p>Finally I got annoyed enough at the CI delays that I decided to revisit, and discovered (after asking some other folks for help) that Bazel was failing because I hadn&#8217;t logged into GitHub with the <code>gh</code> CLI app! WTF??? I thought Bazel was supposed to be hermetic? I mean, OK, look, I&#8217;m not na&#239;ve, sometimes there&#8217;s code in a private GitHub repo, and clearly you&#8217;re going to have to log in to access it. But again, Bazel doesn&#8217;t tell you what code it&#8217;s trying to download or why it failed (or even better, say &#8220;hey I need some creds can you please provide them&#8221;). It just spits out a page of errors in an arcane syntax that refers to packages and modules that you&#8217;ve never even heard of before, and expects you to figure it out.</p><h2>Reason 5: That error handling, though.</h2><p>While we&#8217;re on the subject, let&#8217;s <em>talk</em> about that output. Back in grad school, I cut my teeth on C++/Boost <a href="https://stackoverflow.com/questions/3711267/how-to-read-c-errors-involving-templates">template errors</a>, which were infamous for producing inhumane amounts of error messaging because you forgot a semi-colon. I got really, really good at parsing through C++ errors to figure out what the actual underlying issue was<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>. The error messages were atrocious, no doubt, but <em>usually</em> the right answer was &#8220;scroll to the top and resolve the first issue, and then work your way down&#8221;. Sometimes there were so many errors it would overflow your terminal scrollback, so I learned to always redirect compiler output to a file so I could get back to the top.</p><p>Fast forward a decade, and now most of my development is either in Rust or in Go. Rust has famously set the high waterline for &#8220;good error output&#8221;; it (generally speaking) distills the error down to the actual underlying cause, often includes a hyperlink to the official docs with more information, and (9 times out of 10) will give you the actual fix inline. Go has a much more barebones approach to compiler output, but honestly it&#8217;s still pretty good: it&#8217;s not going to hold your hand through how to fix it, but it will generally tell you what&#8217;s wrong in a single line. I&#8217;ve never seen pages of error output from the Go compiler, and sure, sometimes I have to Google the answer myself instead of clicking a link or taking the compiler&#8217;s recommendation, but it&#8217;s fine.  I don&#8217;t hate it<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>.</p><p>The thing is, with all of these systems, it was easy to identify the actual error message itself. Even with C++, even if the compiler output was thousands of lines long, I always knew I could scroll to the top to figure out what broke.</p><p>Bazel, on the other hand, both produces pages and pages of error messages, and puts the <em>actual</em> error <em>somewhere in the middle</em>. I can <em>know</em> that there is a Go compiler error, and even have a rough idea of what the error probably is, but I still have to spend a significant amount of time looking for the actual message. Worse, the actual error message is not singled out or highlighted in any way, while all of the content both above and below is covered with <code>ERROR</code> labels and other scary-looking output. So my experience with Bazel thus far is, if I want to identify a problem, I need to find the least-obvious line in the entire output to determine what to fix.</p><h2>Reason 6: Compile times are whack</h2><p>My next complaint is about Bazel&#8217;s respect for your (as a developer&#8217;s) time. The compile times are through the roof! And I say that as a Rust dev, the language which has as its primary complaint &#8220;things take too long to compile&#8221;. In some ways I don&#8217;t think there&#8217;s a lot you can do about this; if you&#8217;re trying to make an entirely hermetic build with bit-for-bit reproducibility, that&#8217;s going to take more time than &#8220;just&#8221; running <code>go build</code>. But still. I&#8217;ve got stuff to do, I can&#8217;t wait 10 minutes for you to finish building my app!</p><p>To be fair to Bazel, most builds <em>don&#8217;t</em> take 10 minutes, because of Bazel&#8217;s local on-disk cache.  Congratulations, you&#8217;ve identified my next complaint!</p><h2>Reason 7: Ouch, you punched me in the disk space!</h2><p>To try to alleviate some of the compile-time issues and need to download every single dependency on the planet for every build, Bazel maintains an absolutely <em>immense</em> local cache on your machine. One of my colleagues was bragging about their 1TB local Bazel cache. WTF??? Look I know we all grew up in an era where disk space was cheap, but it sure isn&#8217;t anymore<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. And even if it was, seriously? A terabyte of data just to compile some nonsense? I get mad when Cargo doesn&#8217;t clean up after itself. I get mad because Docker just likes to vomit all its stuff all over your hard drive indefinitely<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>. A freaking <em>terabyte</em>???? My god, what have we done.</p><h2>Reasons 8, 9, and 10: To be continued.</h2><p>I am just on the very beginning of my Bazel journey, and have captured in this blog post all (or at least <em>most</em>) of my complaints with Bazel&#8217;s user experience. But I&#8217;m a fairly new Bazel user, and I&#8217;m sure that given my experience thus far, there are going to be many more reasons in the future that I run into with this software, so I&#8217;ve left space in this blog post to account for those as well.</p><p>And look, this is the Internet. I&#8217;m sure some of you will come along and say &#8220;Well if you just understood the system&#8221; or &#8220;This question is answered in the docs&#8221; or some other variant of &#8220;Git Gud, Noob&#8221;. My rejoinder to that is: I am a pretty smart person in general. I am able to reason about some <em>pretty arcane shit</em>. I like to work on complicated, hard-to-understand systems<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>. And I&#8217;m sure that, given enough time, I <em>could</em> &#8220;git gud&#8221; at bazel.</p><p>But here&#8217;s the thing: <em>I don&#8217;t want to</em>. All I want is to build some source code and produce a binary. I don&#8217;t want to have to learn an entirely new language, parse through pages and pages of errors that don&#8217;t actually even tell you what the problem is, spend hundreds of dollars on a new disk drive, and waste hours of my life waiting for something to build that used to &#8220;just work&#8221;.</p><p>I understand that Bazel solves some real problems, for some real organizations, and that if you have those problems that need to be solved, Bazel might be your best option<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>. I also want to be clear: I&#8217;m sure that whatever Bazel is doing in the backend is probably actually solving those problems moderately well! But I am extremely unconvinced, unless your name starts with a G and rhymes with Froogal, that you have any of the problems that Bazel solves bad enough to subject every single one of your developers at your organization to a build system that treats its users with such disdain<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a>.</p><p>Anyways, just remember, the next time you&#8217;re reading through hundreds of lines of error messages just to discover that the real error isn&#8217;t mentioned anywhere, and you have to go beg one of your more-enlightened coworkers for help: we did this to ourselves. You&#8217;re welcome.</p><p>As always, thanks for reading &#129315;</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>/me frantically knocks on wood</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>AKA, Kubernetes.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><span>If you don&#8217;t work in software&#8212;or heck, even if you do!&#8212;you might be surprised to know that a) this is </span><em>not</em><span> the case for nearly every other build system, and b) this is an </span><em>incredibly</em><span> challenging problem to solve.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Which, of course, I did not.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>No, I don&#8217;t know what cute deer-like animals have to do with build systems either, but whatever.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Don&#8217;t worry, we&#8217;ll get to this.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>And to this day, I&#8217;m convinced that one of my superpowers as a developer is &#8220;being able to parse pages and pages of errors or log messages to identify problems&#8221;. It&#8217;s like, I dunno, I&#8217;m a hipster LLM or something&#8212;I could understand errors and logs before it was cool.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Is that one of the nicest things I&#8217;ve ever said about Golang? It&#8217;s possible!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Thanks, AI.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>Seriously. I set up a systemd timer that runs weekly and clears out all the old images and container builds on my hard disk because I got sick of running out of disk space all the time.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p>Case in point: I run Kubernetes, at home, for fun!  It is well-established at this point that I am a sucker for pain.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p>The only other player I&#8217;m even aware of in this space that I&#8217;m aware of is Nix, which, lol.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p>And if your name <em>does </em>start with G and rhymes with Froogal, you aren&#8217;t going to use bazel anyways, you&#8217;re going to use blaze, which by all accounts is a significantly nicer piece of software.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Karpenter's consolidation behaviour is counter-intuitive]]></title><description><![CDATA[OK, this story starts off with a colleague messaging me at 9am on a Tuesday morning: &#8220;Hey, drmorr, Karpenter is behaving a little bit strangely in this cluster, could you take a quick look?&#8221;]]></description><link>https://blog.appliedcomputing.io/p/karpenters-consolidation-behaviour-5a6</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/karpenters-consolidation-behaviour-5a6</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Tue, 04 Aug 2026 15:30:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!V1am!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>OK, this story starts off with a colleague messaging me at 9am on a Tuesday morning: &#8220;Hey, drmorr, Karpenter is behaving a little bit strangely in this cluster, could you take a quick look?&#8221;</p><p>&#8220;Sure,&#8221; I said, before I&#8217;d gotten my morning caffeine. <em>It&#8217;ll probably take about 15 minutes and then I can get on with my day</em>, I thought to myself<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Five hours later, I finally understood what was going on. Two hours after <em>that</em>, I understood it well-enough to be able to explain it to my colleague<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. A week or so later, I understood it well enough to write this blog post.</p><p>Spoiler warning: there&#8217;s no bug in Karpenter, and everything in the sequence logically follows from one step to the next, but (at least for me) the outcome was still surprising enough that I thought I&#8217;d write a post about it.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">We&#8217;re just a few months out from KubeCon!  This publication is one of the ways readers can support our conference attendance, which in turn helps keeps this publication going.  Would you consider a paid subscription?</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>It all starts with a graph</h2><p>So just to be clear, I&#8217;ve filed off all the serial numbers on this post to protect&#8230; I dunno, probably nobody, I don&#8217;t think there&#8217;s anything particularly sensitive here, but anyways. The issue started out with a graph that looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V1am!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V1am!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 424w, https://substackcdn.com/image/fetch/$s_!V1am!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 848w, https://substackcdn.com/image/fetch/$s_!V1am!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 1272w, https://substackcdn.com/image/fetch/$s_!V1am!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V1am!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png" width="691" height="417" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd549594-3a31-4700-beb7-294ee065ac9a_691x417.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:417,&quot;width&quot;:691,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24675,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V1am!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 424w, https://substackcdn.com/image/fetch/$s_!V1am!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 848w, https://substackcdn.com/image/fetch/$s_!V1am!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 1272w, https://substackcdn.com/image/fetch/$s_!V1am!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd549594-3a31-4700-beb7-294ee065ac9a_691x417.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 1: Hypothetical graph of unused CPU resources over time as stacked timeseries data; all data has been fabricated to protect the innocent.</figcaption></figure></div><p>This graph is showing the <em>unused</em> CPUs in a cluster, aggregated by the nodes in the cluster. In other words, each line represents a different node, and the value at any point is the number of available (schedulable) CPUs on that node. It is a stacked graph so that we can get an idea of both the distribution of unused CPUs as well as the total unused CPUs in the cluster.</p><p>This graph is pretty hard to read! But, if you squint at it for long enough, two things become apparent: first, the &#8220;available CPU capacity&#8221; in the cluster is pretty constant: we more-or-less always have 5 CPUs available. And secondly, despite this, nodes in the cluster are churning <em>constantly</em>. This pattern in this graph repeated for well over 12 hours. What??? Why would Karpenter be doing this? Is this even Karpenter&#8217;s fault???</p><p>However, after checking the Karpenter logs, I was able to confirm that Karpenter <em>was</em> terminating these nodes because of node consolidation. But I&#8217;m still very confused: Karpenter is supposed to consolidate nodes to binpack the cluster better&#8212;in other words, after a node consolidation event, the &#8220;available capacity&#8221; in the cluster should decrease! But that&#8217;s not what we see: after a node consolidation event, the available capacity stays constant.</p><p>To understand what&#8217;s happening here, we need to know about two things: what&#8217;s running on this cluster, and how Karpenter&#8217;s node consolidation works. Let&#8217;s first talk about Karpenter node consolidation, because I hear a <em>lot</em> of <s>lies</s> subtly imprecise discussion about how it works<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><h2>Who&#8217;s consolidating who?</h2><p>The standard sales pitch<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> for Karpenter goes like this: &#8220;Install this magic autoscaler and all your problems will go away!&#8221;</p><p>Sorry, that was snarky. Let&#8217;s try again. The standard sales pitch for Karpenter goes like this: &#8220;Install this magic autoscaler and it will make your cluster much, much cheaper!&#8221; That was still snarky<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>, but is closer to the truth. The question is, &#8220;How does Karpenter make things cheaper?&#8221; The answer is node consolidation. </p><p>At a very high level, Karpenter has two control loops: one is the &#8220;scale up&#8221; loop and the other is the &#8220;consolidation&#8221; loop (I wrote about <a href="https://blog.appliedcomputing.io/p/using-simkube-10-comparing-kubernetes">analyzing the performance</a> of these two control loops way back in the SimKube 1.0 days). The scale-up loop is designed to be fast: it responds to any Pending pods very quickly, and launches new nodes to get those pods scheduled ASAP. The consolidation loop is slower; it&#8217;s constantly on the hunt for the &#8220;optimal&#8221; configuration of pods and nodes, where &#8220;optimal&#8221; in this case means &#8220;cheapest&#8221;. In practice, what this means is that it is constantly trying to shrink the cluster (aka, bin-pack it) by moving pods off of &#8220;lightly used&#8221; nodes so that those nodes can be terminated<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. In theory, this results in lower cost since you&#8217;re not paying AWS for as many compute resources.</p><p>Let&#8217;s dig into the consolidation behaviour in a bit more detail; if you read the <a href="https://karpenter.sh/docs/concepts/disruption/#consolidation">Karpenter docs</a> on this topic, you might be surprised to learn that there are very few knobs you can turn here! You can only set two parameters (on a per-node-pool basis): <code>consolidationPolicy</code> and <code>consolidateAfter</code>. The first parameter controls how Karpenter performs consolidation, and you can either set it to <code>WhenEmpty</code> or <code>WhenEmptyOrUnderutilized</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>; the second parameter controls how long a node has to sit around before the consolidation mechanism kicks in.</p><p>In general, if you&#8217;re running Karpenter, you &#8220;probably&#8221; want to be using <code>WhenEmptyOrUnderutilized</code>; if not, you&#8217;re just waiting for pods to naturally cycle out of your cluster before any sort of bin-packing kicks in, and this is in general going to result in a lot of mostly-empty nodes sitting around in your cluster. So <em>really</em> the only parameter that has any meaningful choices is <code>consolidateAfter</code>. This knob helps reduce churn: set it low, and Karpenter will kick nodes out of the cluster extremely rapidly<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>; but this might be undesirable if you have a new workload coming in that could have used that node. Then you have to spin up a new node, and node provisioning likely takes you longer (and costs you more) than just keeping a &#8220;warm&#8221; node around in the cluster a bit longer. If you expect this sort of thing to happen regularly, you can set <code>consolidateAfter</code> to a higher value to tell Karpenter to keep nodes around<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p><p>I&#8217;ve read through that Karpenter documentation page dozens of times at this point, and every time I&#8217;ve read through it, I&#8217;ve thought, &#8220;It sure is weird that there&#8217;s no way to control the utilization threshold for Karpenter&#8217;s consolidation behaviour!&#8221; In other words, we always talk about Karpenter consolidating &#8220;underutilized&#8221; nodes, which implies that there is some threshold at which Karpenter considers a node &#8220;not underutilized&#8221;, and I&#8217;ve always been a little surprised that threshold isn&#8217;t tunable. But it turns out there&#8217;s a very good reason why that threshold isn&#8217;t tunable: <em>there is no such threshold</em>.</p><p>Specifically: Karpenter will consider a node for consolidation <em>regardless</em> of how full it is. Doesn&#8217;t matter if there&#8217;s one pod or one hundred pods running on the node, as long as we&#8217;re outside the node&#8217;s <code>consolidateAfter</code> window, the node is eligible for consolidation. The only<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a> thing that matters, from Karpenter&#8217;s perspective is, &#8220;do all these pods fit somewhere else?&#8221; If the answer is yes, buh-bye, out you go!</p><p>The implication here is that Karpenter&#8217;s consolidation behaviour has nothing at all to do with the state of the node in question, and <em>EVERYTHING</em> to do with the state of the cluster around it. A node with a hundred pods in a fully loaded cluster? Won&#8217;t get consolidated. That same hundred-pod node in a cluster with lots of other empty nodes? <em>Might</em> get consolidated. Or to put it in even starker terms: Karpenter is using <em>global state</em> (what does the entire cluster look like?) to make a <em>local</em> decision (what should I do with this node?). Generally that is the inverse of what we want in distributed systems.</p><h2>Now you see it, now you don&#8217;t</h2><p>Now that we (or, at least, I) have a better understanding of Karpenter&#8217;s consolidation behaviour, let&#8217;s look back at the workloads running on this cluster, and specifically at the graph I shared at the beginning of this post. Does anything seem odd about this graph? I didn&#8217;t see it until I looked at the inverse of the graph. Instead of looking at the remaining available capacity in the cluster, let&#8217;s look at the committed (scheduled) CPU capacity in the cluster:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9bAr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9bAr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 424w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 848w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 1272w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9bAr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png" width="675" height="476" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:476,&quot;width&quot;:675,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:21180,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9bAr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 424w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 848w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 1272w, https://substackcdn.com/image/fetch/$s_!9bAr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5dc612d-2b33-417e-9603-f31d1770e2a5_675x476.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 2: Hypothetical graph of requested CPU resources over time as stacked timeseries data; this is, essentially, the inverse of Figure 1.</figcaption></figure></div><p>This graph is <em>still</em> kinda hard to read, but it&#8217;s a little bit easier. We can see that the <em>used</em> capacity in the cluster is more-or-less constant now, even as nodes are churning, but also&#8212;huh, look at those spikes<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>! I wonder what those are? It turns out that in this cluster, there are a number of long-lived pods running on the same node pool as an extremely short-lived Job that a) gets triggered at fairly regular intervals, and b) consumes quite a large amount of compute (relatively speaking). The other important thing to understand is that some of the long-lived pods in the cluster have a termination grace period set, which means that (after Karpenter has marked the node for consolidation) the node will stick around for some period of time before the remaining pods get shuffled off of it.</p><p>So now we have all the pieces to understand what&#8217;s happening; instead of squinting at stacked timeseries graphs, it will be a little clearer if we draw the nodes and their contents out explicitly. So, consider the following scenario: we have three nodes, A, B, and C with workloads distributed as follows:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KdHW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KdHW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 424w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 848w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 1272w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KdHW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png" width="518" height="298" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:298,&quot;width&quot;:518,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36308,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KdHW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 424w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 848w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 1272w, https://substackcdn.com/image/fetch/$s_!KdHW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F079cf402-1808-4b04-87f0-f171e4404ef6_518x298.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 3: A three-node Kubernetes cluster with 6 pods.</figcaption></figure></div><p>At this point, there is no short-lived, resource-heavy pod present, and all of the other pods can fit on two nodes, so Karpenter decides to start consolidating node A:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NlMw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NlMw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 424w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 848w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 1272w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NlMw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png" width="518" height="298" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:298,&quot;width&quot;:518,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36219,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NlMw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 424w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 848w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 1272w, https://substackcdn.com/image/fetch/$s_!NlMw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9181cdfa-c7bb-42df-93c9-fd7d8990fae3_518x298.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 4: A three-node Kubernetes cluster with 6 pods after Karpenter has decided to consolidate node A.</figcaption></figure></div><p>Most of the pods on node A have been evicted and rescheduled onto node B; one of the pods on node A is still in a &#8220;terminating&#8221; state while its grace period expires. Now, shortly thereafter, the big, short-lived pod shows up (for the purposes of this example, it takes up an entire node, but it&#8217;s trivial to construct cases where it does not). It has no place to run<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>, so Karpenter launches a new node for it:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!l51p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!l51p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 424w, https://substackcdn.com/image/fetch/$s_!l51p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 848w, https://substackcdn.com/image/fetch/$s_!l51p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 1272w, https://substackcdn.com/image/fetch/$s_!l51p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!l51p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png" width="700" height="299" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:299,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:48990,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!l51p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 424w, https://substackcdn.com/image/fetch/$s_!l51p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 848w, https://substackcdn.com/image/fetch/$s_!l51p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 1272w, https://substackcdn.com/image/fetch/$s_!l51p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ee16dd-860f-4997-8bc9-4cef54316591_700x299.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 5: A four-node Kubernetes cluster with 6 small pods and one large pod scheduled on a new node, node D.</figcaption></figure></div><p>Soon thereafter, the short-lived pod finishes, and we now have three active nodes in the cluster again. Nodes B and D are both ineligible for consolidation, because they&#8217;ve both had scaling activities recently<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a>; however, node C has just been sitting there for a while! Karpenter can consolidate it! So it does:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!08Q0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!08Q0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 424w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 848w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 1272w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!08Q0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png" width="518" height="299" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:299,&quot;width&quot;:518,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:33980,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/209324113?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!08Q0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 424w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 848w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 1272w, https://substackcdn.com/image/fetch/$s_!08Q0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F517396fe-b33c-4f2a-8df7-556d5bde7cda_518x299.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 6: A three-node Kubernetes cluster with 6 pods; node C has been marked for termination.</figcaption></figure></div><p>And, now we&#8217;re back at the beginning of the cycle. This will repeat indefinitely as long as this short-lived, resource-heavy pod continues to appear at regular intervals, and as long as nothing else breaks the cycle. Congratulations! You have introduced constant node churn into your cluster as a consequence of seemingly isolated and independently logical scaling rules.</p><h2>So, how do you fix it?</h2><p>Well now, that&#8217;s the real <em>interesting</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-14" href="#footnote-14" target="_self">14</a><sup> </sup>question, isn&#8217;t it? How <em>do</em> you fix this? This is the real challenge with autoscaling: you can add in another rule to try to break the cycle, but how do you know that this new rule that you add isn&#8217;t going to have <em>other</em> unintended consequences down the line? Basically: you don&#8217;t. There is so much emergent behaviour when you try to scale things up and down, and it&#8217;s nigh-impossible to reason about all of it.</p><p>I know you all are hoping I have a good answer, but I actually don&#8217;t. It&#8217;s a little bit bonkers that Karpenter has decided, &#8220;Hey, I&#8217;ve got this fully loaded node, and this mostly empty node over here, but I&#8217;m not allowed to touch the mostly empty node, so I&#8217;m going to reschedule <em>ALL OF THE PODS</em> on this fully loaded node instead!&#8221; But how do you fix this? Here are some options:</p><ol><li><p>Maybe Karpenter should actually have some kind of consolidation threshold for nodes? E.g., if node A is more than 75% full (or it has more than 42 pods as an absolute number), Karpenter won&#8217;t touch it. This would prevent nodes with a lot of pods from getting consolidated, but could also prevent the cluster from scaling down when desired.</p></li><li><p>Karpenter could consider nodes that have had recent scheduling activity as ineligible for other pods during its scheduling simulation<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-15" href="#footnote-15" target="_self">15</a>. This could also have significant impacts to its consolidation ability, and is also a significant departure from what kube-scheduler would <em>actually</em> do (<strong>edit to add: </strong>this has <a href="https://github.com/kubernetes-sigs/karpenter/pull/3006">actually been done</a> and is available in Karpenter 1.14)</p></li><li><p>Maybe Karpenter needs some kind of &#8220;global churn threshold&#8221;: it <em>kind of</em> has this, with its <a href="https://karpenter.sh/v1.14/concepts/disruption/#nodepool-disruption-budgets">node pool disruption budgets</a>, but this just takes into account nodes that are being terminated Right Now, and doesn&#8217;t look at historical churn rates.</p></li><li><p>Maybe Karpenter should consider historical usage when making consolidation decisions; in other words, it tries to predict that &#8220;oh, this short-lived but resource-intensive pod has been showing up extremely regularly, maybe I should try to leave some room for it.&#8221; This would be extremely cool, but predictive autoscaling behaviour is also <em>extremely hard to do</em>, and there&#8217;s even more cases where it can go wrong than a purely reactive autoscaler.</p></li></ol><p>There are probably some other potential solutions as will, but which one is right? Maybe you should implement multiple options? But what if they conflict in weird ways with other scaling rules? How do you even know? Most of the time, when faced with an autoscaler problem like this, people make decisions based on extremely rigorous criteria such as &#8220;vibes&#8221; and &#8220;YOLO&#8221;.</p><p>But man, wouldn&#8217;t it be nice if there was <a href="https://simkube.dev/">a better way</a>?</p><p>As always, thanks for reading :)</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Lol.  Lmao, even.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Turns out there were actually two separate issues, but I&#8217;m just going to talk about one of them in this post.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><span>And I&#8217;ve repeated/contributed to these </span><s>lies</s><span> imprecise language in the past as well, possibly even somewhere in this very blog post! </span><strong>gasp</strong></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Yes, I know it&#8217;s free.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Sorry not sorry.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p><span>I just did it! I said the </span><s>lie</s><span>, err, sorry, imprecise statement, about Karpenter&#8217;s consolidation behaviour. Did you catch it?</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><span>In the process of doing this investigation, I discovered that the most recent version of Karpenter (1.14, at time of writing) has introduced a third policy, called </span><code>Balanced</code><span>. I know nothing about how it works except that it appears to be an attempt at a less-aggressive form of consolidation that reduces node churn. I won&#8217;t talk about it further here because a) the cluster in question is not on the most recent Karpenter version so couldn&#8217;t use this </span><code>Balanced</code><span> mode anyways, and b) I know nothing at all about it.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>You might be surprised to learn that the default is zero (0) seconds!!! I sure was.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><span>By the way, everybody I&#8217;ve ever talked to about this has said &#8220;we just pick a value for </span><code>consolidateAfter</code><span> based on vibes and feelings&#8221;. Wouldn&#8217;t it be great if there was some way to </span><em>cough</em><span> </span><a href="https://simkube.dev/">simulate</a><span> the effects of different consolidation periods on your cluster?</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>I&#8217;m ignoring disruption budgets here, which aren&#8217;t relevant to the discussion and also don&#8217;t &amp;%($ing do what you want them to anyways.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p><span>As an aside, I was trying to figure out why I just glossed right over them in the first graph; the spikes still show up, just inverted! Stalactites instead of stalagmites, as it were. But I finally figured it out: in the &#8220;available capacity&#8221; graph, the metrics change looks </span><em>exactly like</em><span> some metrics data points have been dropped, and I am extremely conditioned at this point to ignore these types of artifacts in graphs, because data gets lost in Prometheus so often. BUT, if this really was a missing data points issue, you would see the same drop in the inverted graph; instead, we see the opposite, a spike! Which means these are &#8220;real&#8221; data and not just missing metrics.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p>Node A is ineligible, because it&#8217;s being terminated.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p><span>This is another tidbit I learned about Karpenter while investigating this; Karpenter resets the </span><code>consolidateAfter</code><span> timer any time a pod is scheduled or evicted from a node. I think this behaviour makes sense, but previously I had thought that </span><em>removing</em><span> pods from a node didn&#8217;t reset the consolidation timer.</span></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-14" href="#footnote-anchor-14" class="footnote-number" contenteditable="false" target="_self">14</a><div class="footnote-content"><p>A colleague of mine told me yesterday that they&#8217;ve learned any time I say the word &#8220;interesting&#8221; what I actually mean is &#8220;likely to break in all kinds of weird, messed-up ways&#8221;. I consider this a compliment of the highest order.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-15" href="#footnote-anchor-15" class="footnote-number" contenteditable="false" target="_self">15</a><div class="footnote-content"><p><span>This is the converse of its current behaviour: right now, if a node&#8217;s consolidation timer is too low, Karpenter won&#8217;t try to move pods </span><em>off of</em><span> it. What I&#8217;m proposing is that maybe Karpenter also shouldn&#8217;t try to move pods </span><em>onto</em><span> it.</span></p></div></div>]]></content:encoded></item><item><title><![CDATA[The Seven Stages of CI Grief]]></title><description><![CDATA[On the pain scale, a root canal might be an eight.]]></description><link>https://blog.appliedcomputing.io/p/the-seven-stages-of-ci-grief</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/the-seven-stages-of-ci-grief</guid><dc:creator><![CDATA[Ian O’Gorman]]></dc:creator><pubDate>Fri, 10 Jul 2026 20:01:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pDrV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On the pain scale, a root canal might be an eight. CI-related pain is an eleven. After months of successfully, if slowly<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, churning out AMIs for ACRL, my Packer pipeline started failing. But I write to you with hope. I am happy to report that after working through the Seven Stages of CI Grief I have finally fixed our AMI pipeline and did not leave the profession to raise goats.</p><p>Six months and several millicores ago I <a href="https://open.substack.com/pub/appliedcomputing/p/boy-ami-glad-to-see-you">built an AMI pipeline</a> for ACRL. We needed a way to automate builds for the SimKube AMI and SimKube GitHub Action Runner AMI. Each piece of our pipeline was carefully chosen and to this day mostly defensible: Ansible, Packer, and GitHub Actions. Simple and boring. One of these helpful tools was about to ruin my <s>week</s> month.</p><h2>The Seven Stages</h2><h3>Stage 0: Blame</h3><p>It was a rainy Tuesday morning<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. I logged in to AWS to find an EC2 instance had escaped containment. My AMI pipeline hit an error and unfortunately bypassed cleanup. Not the end of the world but certainly annoying and wasteful. The GitHub Actions log showed an error during the <a href="https://developer.hashicorp.com/packer/integrations/hashicorp/ansible/latest/components/provisioner/ansible">Ansible provisioner</a> phase of my Packer build. The error type was new, an EOF. Which I Googled. The first result said that EOF stood for &#8220;End of File&#8221; and could be a connection issue. I immediately thought &#8220;Oh, some change in our Ansible repo is causing our connection to time out, no big deal.&#8221; The last commit to the Ansible repo was authored by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;a546f576-aaea-482f-adaf-5fe864069e4d&quot;}" data-component-name="MentionToDOM"></span> so obviously I didn&#8217;t cause this problem.</p><p>Adding it to our issue tracker, I set it aside. I told myself I had other, more important, things to work on. This would be an easy fix I could get to later. Right?</p><h3>Stage 1: Retrying</h3><p>A few weeks later I&#8217;m finally getting back to it. Now I really need to sort this out. <strong>The AMIs must flow</strong>. I look at the last commit in the Ansible repo, the suspect one that <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;1ee71384-df74-4590-b74d-56121abd7812&quot;}" data-component-name="MentionToDOM"></span> shipped. There is nothing obviously wrong with it, just some <a href="https://github.com/theori-io/copy-fail-CVE-2026-31431">CopyFail</a> mitigation. I do the most optimistic thing possible: I rerun the pipeline. It&#8217;s probably worth noting that our AMI pipeline is initiated via a GitHub Action which clones our Ansible repo and configures Packer. Then, it runs our Packer build which provisions an EC2 instance and runs the Ansible provisioner against it. Finally, once the provisioner steps complete we snapshot the instance and copy it to the regions it needs to appear in<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>The rerun fails: EOF. I rerun it again this time omitting <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;e4b98b96-e0ee-4d79-8ad0-4363d59356cd&quot;}" data-component-name="MentionToDOM"></span>&#8217;s changes by reverting to the previous commit. Same failure, EOF. No detail. I test it again with an even older branch and, yep, EOF again.</p><p>This is starting to get weird. That last commit I tried was the code used to produce and ship our last AMI artifact. It isn&#8217;t working now. Now I&#8217;m scared. A known-good version is now no longer good, so we aren&#8217;t chasing a simple bad commit.</p><h3>Stage 2: Self-Recrimination</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pDrV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pDrV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 424w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 848w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 1272w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pDrV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1038982,&quot;alt&quot;:&quot;Tim Robinson in the hot dog suit sketch from I Think You Should Leave, a visual reference to the line \&quot;we're all   trying to find the guy who did this.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/206216806?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Tim Robinson in the hot dog suit sketch from I Think You Should Leave, a visual reference to the line &quot;we're all   trying to find the guy who did this." title="Tim Robinson in the hot dog suit sketch from I Think You Should Leave, a visual reference to the line &quot;we're all   trying to find the guy who did this." srcset="https://substackcdn.com/image/fetch/$s_!pDrV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 424w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 848w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 1272w, https://substackcdn.com/image/fetch/$s_!pDrV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d1e5a3b-6b64-4a26-bf44-f37fa026e0ce_1560x1040.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I am beginning to suspect I might be both the victim and the murderer.</figcaption></figure></div><p>I&#8217;m looking at my creation with disdain and realize that I am Dr. Frankenstein and this CI pipeline is <strong>my</strong> monster. Lovingly crafted from parts of FOSS, abstractions I thought I understood. For what? To be trapped in an EOF prison of my own design. There are so many places this issue could be. I wanted to do anything else. I suspect every piece of the pipeline, including its creator, is to blame. Pinching the bridge of my nose I tell myself: &#8220;It&#8217;s not the end of the world. We&#8217;ve got a few culprits.&#8221; This issue could be Packer, Ansible, GitHub Actions, SSH, EC2 networking, the bastion, or a poltergeist. At this stage it&#8217;s dangerous to rule anything out. It <strong>has</strong> to be one of them.</p><h3>Stage 3: Isolation</h3><p>Debugging CI sucks. So I have an idea: let&#8217;s first make sure it&#8217;s a CI problem. Because if I can choose, I would rather solve a non-CI problem. I&#8217;m an optimist. The easiest isolation is running the Ansible playbook locally, it isn&#8217;t the exact same as configuring a remote, but it will rule out a lot of Ansible<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> and it&#8217;s <strong>fast</strong>. I run the Ansible playbook locally and it works, no errors. I now know that the vast majority of our configuration management system is working. Now I can move a little closer to reality, so I create an EC2 instance and add it to a test inventory and repeat my Ansible playbook run. Now I&#8217;m configuring a remote host, so all of our common remote configurations will also be applied. This is where I expect the issue to arise. It does not. The playbook completes, I do some spot testing to confirm, and everything is as it should be.</p><p>The problem was not &#8220;Ansible can&#8217;t configure the instance.&#8221; The problem was &#8220;Ansible can&#8217;t configure the instance when Packer is orchestrating from CI&#8221;. I&#8217;m devastated because the issue only appears in CI which means repeated testing is going to be an absolute <strong>pain in the ass</strong><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>.</p><h3>Stage 4: Bargaining with SSH</h3><p>If you&#8217;ve ever used Packer, or any image build tool, you know how slow the feedback loop can get. I&#8217;m devastated because my feedback loop just went from a few minutes to a meaningful fraction of an hour. At this point I know I&#8217;ve got something weird going on, and it doesn&#8217;t seem to be related to one of our Ansible roles.</p><p>So I get sidetracked and start furiously Googling: &#8220;Packer+EOF&#8221;, &#8220;Ansible+EOF&#8221;, &#8220;EOF+WTF?&#8221;. None of the GitHub issues and StackOverflow posts I am finding seem to align with the EOF error I am seeing. Lots of comments like &#8220;EOF means End of File, dummy,&#8221; as if that tells me everything I need to know. When I stop skimming and start reading in more detail it seems that EOF is often a symptom of connection-related issues. Somewhere in the communication between my GitHub runner that is executing the configuration and the EC2 instance that is being configured we are losing the plot. Ansible is waiting for a signal that it is never going to receive and the result is EOF.</p><p>It&#8217;s going so poorly at this point I ask Claude and it says this is a classic issue with SSH multiplexing. <strong>Spoiler</strong>: it was not SSH multiplexing.</p><p>I don&#8217;t believe Claude. But I try changing our SSH configuration anyway. I change SSH variables in Ansible and pass extra SSH args via Packer. Now I&#8217;m turning off multiplexing: EOF, increasing timeouts: EOF, and fiddling with keepalives: EOF. In frustration, I have to walk away.</p><p>I&#8217;m stuck in an SSH tunnel, and my canary just died.</p><h3>Stage 5: Log Diving</h3><p>Okay, it&#8217;s a new day now. We&#8217;re going back to basics. I turn Ansible verbosity way up <code>-vvvv</code> and set it to output to a logfile. I turn on <code>PACKER_LOG</code> too. The build now takes forever. The reason is that all these logs are being round-tripped over SSH and it&#8217;s slow as hell. The build takes so long my build times out at my 90-minute cutoff. I drop verbosity to <code>-vvv</code>; still timing out. I set the build timeout to two hours, I <strong>finally</strong> make it to my EOF with some enhanced logging. The logging doesn&#8217;t provide any more context. I can&#8217;t live like this.</p><p>While I&#8217;m at it I keep a builder EC2 alive after the provisioner fails to inspect it. The EC2 instance is fine<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>. It&#8217;s patiently waiting for the next instruction from Ansible. The logs don&#8217;t point to what is broken, but they do tell me one thing: the build isn&#8217;t failing because of a specific Ansible task. The issue is not with the target machine. It seems to be somewhere in my connection to it. I haven&#8217;t found a smoking gun. I&#8217;m starting to become SSH-skeptical.</p><h3>Stage 6: Acceptance</h3><p>It could be an SSH issue. We have some proxying going on. Our GitHub Action runner has to tunnel through a bastion to get to our EC2 instance. I start looking into how Packer handles connections, and it has its own <a href="https://developer.hashicorp.com/packer/docs/communicators/ssh">SSH communicator</a>. I experiment with Packer&#8217;s SSH settings and realize that I can remove another layer: by turning off the Packer proxy and using a standalone SSH agent. On a whim, I add <a href="https://github.com/webfactory/ssh-agent">webfactory/ssh-agent</a>, configure the agent once early in the run, and set <code>packer_proxy = false</code>. Boom, it works the first time and we haven&#8217;t seen an EOF since.</p><p>I wish I could say I know exactly what I was doing wrong with our Packer SSH settings. I am pretty sure it was a &#8220;me&#8221; problem because lots of folks are using it without issue. Perhaps they have less proxying, fewer long-running Ansible tasks, or simply aren&#8217;t cursed by the Packer gods. But if it doesn&#8217;t spark joy, remove it. I find the standalone SSH agent much more comprehensible. It actually makes all the SSH management more straightforward and transparent.</p><h2>Conclusion</h2><p>Maybe you saw a little of your own CI frustrations in this post. Maybe you think I&#8217;m being silly and should just be using the <a href="https://developer.hashicorp.com/packer/integrations/hashicorp/ansible/latest/components/provisioner/ansible-local">ansible-local provisioner</a> in Packer and cleaning up my AMI before it&#8217;s published. That also would have solved my problem, but one of the reasons we wanted to avoid the <code>ansible-local</code> provisioner was specifically to prevent unnecessary cleanup work for our public AMIs. Yes, executing Ansible over SSH is slow, but it&#8217;s been fast enough for our needs. We want a <s>simple</s> boring AMI pipeline. By adding a standalone SSH agent we managed to make it a little more boring. We are using Packer only for the piece we really need Packer for and that&#8217;s for the best.</p><p>I hope that my misguided meanderings trying to debug this issue and recounting it here for your enjoyment will save one person a little time. If you find this post in the year 2056 and are experiencing EOF errors in your Packer pipeline, I would recommend the following EOF pain-reduction plan:</p><ol><li><p>Rule out the easy stuff: run a local Ansible test.</p></li><li><p>Test the build against an EC2 instance using a test inventory.</p></li><li><p>If that all worked fine, but you are still getting EOFs, I would recommend testing a standalone SSH agent and turning off the Packer proxy: <code>packer_proxy = false</code></p></li><li><p>If you are still getting EOFs, I apologize. I have failed to solve your problem. Please send your complaints directly to <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;0e048a99-084b-42a1-bce5-d72c3ea320f4&quot;}" data-component-name="MentionToDOM"></span>.</p></li></ol><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If this story was useful or entertaining to you, consider becoming a free or paid subscriber of the ACRL blog. </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Cheers,</p><p>Ian</p><p>P.S. <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;drmorr&quot;,&quot;id&quot;:155618049,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d473293-68d3-4115-80cf-b96e65b9fffd_460x460.png&quot;,&quot;uuid&quot;:&quot;cb8211df-19b7-4b93-814e-111258d71000&quot;}" data-component-name="MentionToDOM"></span> set up a Tailscale tailnet for ACRL. So in the future I&#8217;ll be adding my GitHub Runner that executes the AMI pipeline to the ACRL tailnet. That means we won&#8217;t have to make the bastion hop to reach the EC2 instance. Exciting!</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>VERY slowlyyyyy.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>I live in Portland, Oregon it rains on average ~3 out of 7 days.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Mold moves faster. About half of our pipeline&#8217;s total runtime is copying the snapshot across regions.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>We have a LOT of Ansible.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>To the editorial board, I plead for mercy.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Later I forget to terminate it. Our AWS bill is now forty billion dollars. Send help.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Worth the Wait: Making SimKube Pod Startup More Realistic]]></title><description><![CDATA[If you&#8217;re detail-oriented, skeptical, or just enjoy watching Pod transitions in your spare time, you may have noticed a peculiar behavior in SimKube: Pods transition to Running very quickly.]]></description><link>https://blog.appliedcomputing.io/p/worth-the-wait-making-simkube-pod</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/worth-the-wait-making-simkube-pod</guid><dc:creator><![CDATA[Ian O’Gorman]]></dc:creator><pubDate>Mon, 22 Jun 2026 18:01:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!InEd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you&#8217;re detail-oriented, skeptical, or just enjoy watching Pod transitions in your spare time, you may have noticed a peculiar behavior in <a href="https://simkube.dev/">SimKube</a>: Pods transition to Running very quickly. That isn&#8217;t because SimKube is broken but because those Pods are being simulated.</p><p>SimKube uses <a href="https://kwok.sigs.k8s.io/">KWOK (Kubernetes WithOut Kubelet)</a> to simulate large clusters efficiently. Instead of running thousands of workloads, KWOK lets us substitute simulated Nodes and Pods that behave enough like real ones to make large-scale replay practical.</p><p>Since there is no kubelet running on KWOK Nodes, KWOK is responsible for simulating much of the Pod lifecycle. The problem is that, until recently, Pods moved a little too quickly. At first glance, this seems easy to dismiss. If a Pod takes 50 milliseconds or five seconds to reach Running, it eventually gets there either way, right? Let&#8217;s take a closer look.</p><p>Real Pods spend time in Pending, images need to be pulled, and containers need to initialize. Those delays are real and vary quite a bit from cluster to cluster. You can imagine pulling a light image cached on a warm Node versus a heavy image pulled from a remote registry. Now compound those differences over thousands of workloads. If every simulated Pod skips these delays, replay behavior starts to drift slightly away from the source cluster. That difference might not matter for every workload, but it absolutely matters when you&#8217;re trying to faithfully replay cluster behavior.</p><p>In <a href="https://github.com/acrlabs/simkube/releases/tag/v2.7.0">SimKube v2.7.0</a> we fixed this by introducing configurable image pull and container startup delays. Let&#8217;s talk about why and how.</p><h2>Why Delay?</h2><p>You might still be asking yourself who cares about these small delays? Well, I do, and I think you should too. So much so that I ran a small experiment this weekend. I created a <a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind</a> cluster and installed the <a href="https://simkube.dev/simkube/docs/components/sk-tracer/">SimKube tracer</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, which captures the event data needed for replay. I then applied a single deployment with 50 replicas and exported a trace. At the same time I applied the deployment, I started a small polling script that recorded how many Pods had reached Running every second. I set this up in a single script, a lazy man&#8217;s test harness, so I could repeat this when I messed it up and I did several times.</p><p>What the polling data showed was that all 50 Pods reached Running in about four seconds, with the first pods transitioning sometime between the two and three-second marks. With this baseline safely tucked away in a CSV that I won&#8217;t accidentally overwrite<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, we can move into the fun stuff&#8212;simulation!</p><p>First, we need to capture the existing behavior in SimKube, so we gather the same polling data for the simulated run, using <code>skctl run</code> without any configured delays. We get about what we expect: Pods transition too quickly. In fact, all 50 Pods in our deployment had reached Running by the two-second mark and the first pods that hit Running between zero and one second<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. That&#8217;s too fast. We&#8217;ve compressed a four-second rollout into roughly two seconds.</p><p>We can then repeat our experiment with our configured delays. If you look at figure 1, you can see that by introducing delays we largely corrected the Pod startup profile for our experimental workload. It&#8217;s not a perfect match, but the replay now behaves much more like the original workload.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!InEd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!InEd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 424w, https://substackcdn.com/image/fetch/$s_!InEd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 848w, https://substackcdn.com/image/fetch/$s_!InEd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!InEd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!InEd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:147789,&quot;alt&quot;:&quot;Line chart comparing source cluster startup timing, SimKube replay without delays, and SimKube replay with   configurable delays. The delayed replay more closely matches the source workload, reaching 50 Running Pods in roughly   four seconds instead of two.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/203123211?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Line chart comparing source cluster startup timing, SimKube replay without delays, and SimKube replay with   configurable delays. The delayed replay more closely matches the source workload, reaching 50 Running Pods in roughly   four seconds instead of two." title="Line chart comparing source cluster startup timing, SimKube replay without delays, and SimKube replay with   configurable delays. The delayed replay more closely matches the source workload, reaching 50 Running Pods in roughly   four seconds instead of two." srcset="https://substackcdn.com/image/fetch/$s_!InEd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 424w, https://substackcdn.com/image/fetch/$s_!InEd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 848w, https://substackcdn.com/image/fetch/$s_!InEd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!InEd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc5383d1-35b6-4d22-b60d-ba9baf1a7949_2400x1200.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 1: Running Pods over time for a 50-replica Deployment recorded in a kind cluster and replayed through SimKube. With delays configured, the startup profile more closely matches the source workload.</figcaption></figure></div><p>In our simple test, configurable delays moved Pod startup time in replay from roughly two seconds to roughly four seconds, much closer to the behavior of the source cluster. So how did we go about making simulated Pod startup look more like the real thing?</p><h2>The Design Goals</h2><p>In a simulated cluster, we want timing to be as close to the source cluster as possible. Having Pods that jump to Running too quickly introduces drift we don&#8217;t want. Our goal was to add realistic Pod startup timing to SimKube replays. Not realistic in the abstract, but realistic for the cluster being modeled. It doesn&#8217;t take much imagination to come up with examples for why these delays vary. Nearly everyone has dealt with a weirdly large image, a flaky registry, high latency regions, or a container with a lengthy startup time. By making delays configurable we can let the user decide what the delays should look like. Everyone can be unhappy in their own way.</p><h2>Teaching Pods to Wait</h2><p>Implementation-wise, we got lucky. SimKube already uses <a href="https://kwok.sigs.k8s.io/docs/user/stages-configuration/">KWOK Stages</a> to move Pods through lifecycle transitions. The problem was that those transitions were happening almost immediately and were not yet configurable. To make startup timing more realistic, we first introduced delays and jitter between the stages to simulate image pulls and container startup. Our stages are based on the <a href="https://github.com/kubernetes-sigs/kwok/tree/main/kustomize/stage/pod/general">Pod General Stages</a> in the KWOK documentation.</p><p>We introduced delays at two points in the simulated lifecycle. The first delay models image pull times. The second models container startup before the Pod reaches Running. Figure 2 is a simplified view of the transition timeline. SimKube now adds a new <code>pod-create</code> KWOK stage before the Ready transition, allowing us to model image-pull delay. The startup delay was configured in our existing <code>pod-ready</code> stage.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4ZrQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 424w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 848w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 1272w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png" width="800" height="520" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:520,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:70600,&quot;alt&quot;:&quot;Conceptual startup timeline showing that previous SimKube behavior progressed through startup states almost immediately, while configurable image-pull and startup delays add realistic timing before Pods reach Running.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/203123211?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Conceptual startup timeline showing that previous SimKube behavior progressed through startup states almost immediately, while configurable image-pull and startup delays add realistic timing before Pods reach Running." title="Conceptual startup timeline showing that previous SimKube behavior progressed through startup states almost immediately, while configurable image-pull and startup delays add realistic timing before Pods reach Running." srcset="https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 424w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 848w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 1272w, https://substackcdn.com/image/fetch/$s_!4ZrQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d7c3f1d-b800-4935-9f2f-b8fd56c346e2_800x520.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 2: Simplified timeline showing where SimKube inserts configurable delays to model image pull and container startup time during replay.</figcaption></figure></div><h2>Advanced Delays for the Curious and Crazy</h2><p>In SimKube v2.7.0, delays and jitter are fully configurable, meaning that when you run a simulation you can inject the jitter and delays specific to your production environment (in milliseconds) directly in your <code>skctl run</code> command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;shell&quot;,&quot;nodeId&quot;:&quot;a50f975b-c2ac-446d-8d31-ccfc153229cd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-shell">skctl run my-simulation \
  --trace-path s3://my-bucket/my-trace \
  --image-pull-delay 1000 \
  --image-pull-jitter 1000 \
  --pod-startup-delay 1000 \
  --pod-startup-jitter 1000</code></pre></div><p>These options are applied globally at the start of the simulation. For backwards compatibility, all delays and jitters default to zero so you can safely omit them if you need to reproduce or compare results with simulations from prior releases. We may revisit these defaults in a future release, but we&#8217;ll be sure to communicate any changes.</p><p>Behind the scenes, SimKube passes delay and jitter values to KWOK through Pod annotations, allowing a single set of stage definitions to be reused for all our simulated Pods. SimKube sets Pod annotations in its admission webhook the first time a Pod is seen.</p><h2>Without further Delay</h2><p>So why go to the trouble of adding a few seconds of delay to a simulation that already has a high level of fidelity? Because startup timing is real. Image pulls, registry latency, and container initialization all vary from cluster to cluster. By making delays configurable, SimKube can more closely model the cluster being replayed instead of assuming every cluster has similar delays.</p><p>So without further delay give our configurable delays a try in SimKube v2.7.0 and let us know what you think!</p><p>Cheers,</p><p>Ian</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Don&#8217;t delay, subscribe today. Applied Computing Research Labs is a reader-supported publication. </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Shameless plug.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Twice.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>I&#8217;m only polling in one second intervals, which I regret, but here we are.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Stop accusing people of cheating with LLMs]]></title><description><![CDATA[This is a bonus post for the week, because I&#8217;m frustrated and mad.]]></description><link>https://blog.appliedcomputing.io/p/stop-accusing-people-of-cheating</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/stop-accusing-people-of-cheating</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Tue, 16 Jun 2026 18:01:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!M8wv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdbcc8-dc09-4e12-8fc3-348cb9c2691e_518x518.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a bonus post for the week, because I&#8217;m frustrated and mad. Recently <a href="https://lobste.rs/s/guyifg/removing_my_nix_flakes_vs_guix_post">on lobste.rs</a>, someone posted that they had taken down an article they had written because someone else had accused them of using an LLM to write it. The original author posted that they did not use an LLM, that the bogus accusation was from someone that they really looked up to and respected, and they were feeling really hurt, frustrated, and confused.</p><p>And look. I know AI slop is a thing, and it&#8217;s really tiring to wade through. But accusing other random people on the Internet of cheating (because let&#8217;s be real, that&#8217;s what you&#8217;re doing) isn&#8217;t going to solve the AI slop problem. All it&#8217;s going to do is make real people hurt, upset, and angry. I say this as a person with relatively thick skin, as someone who doesn&#8217;t much care what other randos on the Internet think about my writing, and (also) as someone who doesn&#8217;t use LLMs to write on this blog, like, at all. I&#8217;m pretty solidly in the &#8220;AI;DR&#8221; (Artificial Intelligence; Didn&#8217;t Read) camp, or in other words, &#8220;If you can&#8217;t be arsed to write it, I can&#8217;t be arsed to read it&#8221;.</p><p>But I also say this as someone who got one of his blog posts labeled as &#8220;hot garbage&#8221; by some random Redditor because they thought it was AI slop, and honestly? It kinda stung. My coping mechanism was to take that Redditor to task, both on Reddit, and also somewhat mercilessly on here. You can argue about whether that was helpful or not (probably not) but if you&#8217;re going to throw around false accusations about <em>my</em> writing you can bet I&#8217;m not going to take that lying down.</p><p>There are lots of arguments people make as to why &#8220;labelling things as AI slop is good, actually,&#8221; and I recognize that I&#8217;m not going to change anybody&#8217;s mind on this blog post, but I feel like deconstructing some of them anyways:</p><ol><li><p>&#8220;I don&#8217;t want to read AI slop, and I don&#8217;t think other people want to read it either, so I&#8217;m doing a public service!&#8221; Ok, I&#8217;m sure you think your AI slop detector is <em>the most refined</em> slop detector and you <em>never</em> have any false positives, but I guarantee you that you&#8217;re wrong. One thing that LLMs are good at is pretending to be human, and regardless of whether they will ever be useful, they will definitely get better at pretending to be a human. So I know you think your list of tells (em-dashes, bullet points, &#8220;it&#8217;s not X, it&#8217;s Y&#8221;) is better than everyone else&#8217;s list of tells, but sooner or later you&#8217;re going to get one wrong, and when you do the person on the other end is going to be hurt and frustrated.</p></li><li><p>&#8220;Yea, you&#8217;re right, I&#8217;m going to get some things wrong, but this is a war against Big AI! There&#8217;s going to be some casualties!&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> OK, look, putting aside the violent rhetoric, which clearly doesn&#8217;t help anybody, the way you &#8220;win&#8221; your &#8220;war&#8221; against &#8220;Big AI&#8221; is by embracing the actual humanity in each of us. We need communities who can come together and support each other and elevate the <em>actual humanness</em> on the Internet. When you come in and start tossing &#8220;This is just AI slop hot garbage&#8221; bombs all over the place, you&#8217;re actually <em>HELPING</em> your perceived enemies, by dividing the communities you should be supporting.</p></li><li><p>&#8220;Some people don&#8217;t realize that LLMs are bad, and I&#8217;m just educating them. I&#8217;m doing a public service!&#8221; Same comment as (1). Your LLM detector isn&#8217;t as good as you think it is. Also this one has an extra layer of elitism and superiority: even if you &#8220;get it right&#8221; and the target of your critique used an LLM, all you&#8217;re doing is announcing that you&#8217;re better than them, under a very thin veneer of &#8220;just trying to help.&#8221;</p></li><li><p>&#8220;People just need to have thicker skin; if you can&#8217;t handle criticism on the Internet, you shouldn&#8217;t be posting stuff for people to read.&#8221; Ah yes, my favorite, the drive-by victim blame: &#8220;It&#8217;s your fault that other people are assholes.&#8221; Are we really still doing this in 2026?</p></li><li><p>&#8220;This is exactly why we need <code>humans.json</code>!&#8221; I am happy to support efforts to re-build the &#8220;small web&#8221; or use a &#8220;web of trust&#8221; or whatever else&#8212;I do think those things are interesting&#8212;but proposing them as a solution to LLM slop is looking for a technical solution to a human problem, which is always the wrong way to go about things. How exactly will this weird, niche technology that most people won&#8217;t even know about stop people from making false accusations about AI slop?</p></li><li><p>&#8220;Everyone should just put a banner at the top of their website saying that the content is AI-free!&#8221; This is a great combination of victim-blaming and naive solutions. For one: why should I have to modify <em>my</em> behaviour because other people are assholes? For two: if this becomes normalized (and I&#8217;m already seeing it in quite a few places), the &#8220;bad actors&#8221; are just going to start putting &#8220;This is human-generated&#8221; at the top of all <em>their</em> posts, and instead of throwing around accusations about AI slop, we&#8217;re just going to start throwing around accusations of &#8220;lying <em>and</em> AI slop&#8221;. How is that better?</p></li></ol><p>Anyways, a while back I read <a href="https://www.amazon.com/AI-Fight-Techs-Create-Future/dp/0063418568">The AI Con</a> by Emily Bender and Alex Hanna, because I&#8217;d heard a lot of good things about it. Unfortunately, I came away from that book profoundly disappointed. One of the key takeaways from that book was &#8220;make fun of people who use AI&#8221;, and, excuse me, but what???? This is the <em>definition</em> of &#8220;punching down&#8221;, and it leads to exactly the situation we&#8217;re in today, where Internet randos get accused by other Internet randos of doing things they didn&#8217;t do, with real impacts to the mental health and physical well-being of everyone involved<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.</p><p>And look, I get it. AI sucks. The hype is exhausting. There are real, serious, extremely troubling implications with the technology. The rhetoric from the &#8220;other side&#8221; is over-the-top, overblown, and seems to &#8220;demand a response&#8221;. But if your response is to shame and make fun of internet bystanders, you can GTFO.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Think this language is an exaggeration? No, you&#8217;re wrong, I&#8217;ve seen people say exactly this on the internet.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>By all means, go ahead and continue making fun of the billionaires and (ugh) trillionaires peddling this nonsense, though.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Two things to know about Kubernetes Mutating Webhooks]]></title><description><![CDATA[This is a quick story about a bug in SimKube where I knew what the bug was from the beginning and it still took me several hours to actually troubleshoot.]]></description><link>https://blog.appliedcomputing.io/p/two-things-to-know-about-kubernetes</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/two-things-to-know-about-kubernetes</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Sun, 14 Jun 2026 17:01:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!KuQY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KuQY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KuQY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 424w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 848w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KuQY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg" width="1456" height="1094" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1094,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KuQY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 424w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 848w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!KuQY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe05b5462-f7fb-4d8c-b1a9-c06c0c6d0929_1600x1202.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is a quick story about a bug in <a href="https://simkube.dev/">SimKube</a> where I knew what the bug was from the beginning and it still took me several hours to actually troubleshoot. In the end, the problem was&#8230; exactly what I suspected it was at the beginning, but it manifests in such a weird way that I couldn&#8217;t piece it together.</p><p>This is a story in two parts<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> about mutating webhooks in Kubernetes.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Applied Computing Research Labs is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>What&#8217;s a webhook, precious?</h2><p>OK, so, Kubernetes lets you extend the system in two ways<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>: the first is through Custom Resources, namely, you can define your own &#8220;Kubernetes objects&#8221; that look, feel, and behave just like all the built-in Kubernetes objects (Deployments, DaemonSets, etc). The second way is by injecting random bits of code into the pipeline to inspect or modify these resources at runtime. These random bits of code are called webhooks.</p><p>There are two types of webhooks<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>: validating admission webhooks and mutating admission webhooks. More-or-less, these do exactly what they sound like: they either check that Kubernetes resources or actions meet some policy, or they modify Kubernetes resources before they&#8217;re created. The reason they&#8217;re called webhooks is that you interact with them over <s>the web</s> the cluster&#8217;s network, which I dearly hope is not something that you&#8217;ve exposed to the open Internet, but, you know, you do you.</p><p>There are two methods to create a webhook<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>: YAML and JSON. For example, here is the webhook specification that SimKube uses (with some fields omitted for brevity):</p><pre><code><code>apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: sk-testing-mutatepods
webhooks:
  - name: create-update.mutatepods.simkube.io
    clientConfig:
      service:
        name: sk-testing-driver-svc
        namespace: simkube
        port: 8888
    rules:
      - apiGroups:
          - ""
        apiVersions:
          - v1
        operations:
          - CREATE
          - UPDATE
        resources:
          - pods
          - pods/status
    failurePolicy: Fail
    matchConditions:
      - expression: object.metadata.namespace.startsWith('virtual-')
        name: virtual-namespaces-only
    matchPolicy: Equivalent</code></code></pre><p>There are two key things to note<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>: the first is the <code>clientConfig</code> block; this contains a <code>service</code> reference, which tells Kubernetes what the endpoint of the webhook network request should be. In this case, it&#8217;s the SimKube driver, which is actually running our simulations. The second key thing to note is the <code>rules</code>: this tells Kubernetes under which circumstances the webhook should be invoked. For SimKube, we want our webhook to be invoked whenever a pod object is created or updated. The third key thing to note is the failure policy, which instructs Kubernetes that the pod creation or update operation should be rejected if the webhook doesn&#8217;t work for whatever reason (previously, the failure policy was set to &#8220;Open&#8221;, which was one of the inciting factors in ACRL&#8217;s <a href="https://blog.appliedcomputing.io/p/postmortem-intermittent-failure-in?utm_source=publication-search">first public postmortem</a>). The last key thing to note are the match conditions; these are <a href="https://kubernetes.io/docs/reference/using-api/cel/">CEL expressions</a> which allow you to narrow the targets of the webhook even further; here we&#8217;re instructing Kubernetes to only call the webhook for pods that are created in a namespace with the <code>virtual-</code> prefix.</p><h2>What happens when a webhook breaks?</h2><p>As I mentioned <a href="https://blog.appliedcomputing.io/p/bare-pods-are-weird-man">a few weeks ago</a>, I&#8217;ve been extending SimKube to work better with bare pods (aka a pod that doesn&#8217;t have some other owning Kubernetes resource). There are two weird things about bare pods<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>: if the node they are running on disappears, the pod itself is deleted by the Kubernetes Garbage Collector, and because they don&#8217;t have an owner, there is nothing to reschedule them somewhere else. This can lead to inaccurate simulation results if you&#8217;re operating with an autoscaler like Karpenter, so I wanted to add a feature to SimKube to reschedule bare pods that were interrupted before they could complete. Fortunately, Kubernetes makes this easy; the two steps<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a> I took were to a) register a <code>DELETE</code> operation for the webhook, and b) update the driver code to check if the deleted pod had finished running, and recreate it if not.</p><p>Once I had written the code to do this, I loaded it up into my cluster to test it out; before the change, if I deleted a node with running pods on it, the node (and the pods) disappeared for good. <em>After</em> the change&#8230; deleting the node hung forever, and the pod never went away.</p><p>&#8220;Aha!&#8221; I said to myself. &#8220;I have seen this before; the mutating webhook is failing, and the logs for the failure are going to be in a weird place, because that&#8217;s how Kubernetes Mutating Webhooks work!&#8221; I then spent several hours debugging before confirming that this was, indeed, what was happening. I&#8217;ll spare you the details, but at the end of the journey, I found myself looking at this log line from <a href="https://kwok.sigs.k8s.io/">KWOK</a> (as an aside, I recently set up <a href="https://docs.victoriametrics.com/victorialogs/">VictoriaLogs</a> on my <a href="https://blog.appliedcomputing.io/p/everything-you-need-to-run-a-single">Kubernetes cluster</a>, and it made this entire process so much easier: 10 out of 10, would read logs in the VM UI again).</p><pre><code><code>failed to apply stage:
failed to delete pod bare-pod-1781282509: pods "bare-pod-1781282509" is forbidden: expression
'object.metadata.namespace.startsWith('virtual-')' resulted in error: no such key: metadata</code></code></pre><p>There are two confusing bits here<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>: first, the CEL expression above is clearly coming from my webhook, and yet my webhook was never actually even getting called; and second, the error was <em>actually</em> coming from KWOK, which was the last place that I was expecting to see an error. So even though I predicted exactly this scenario at the beginning, I ended up being so confused that it took me a while to understand what was happening.</p><p>The short version is: if the <em>configuration</em> for a webhook is broken in some way, the error gets reported to <em>whatever component caused the webhook to get invoked</em>. In this case, Karpenter was calling the <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/api-eviction/">Eviction API</a>, which in turn was causing KWOK to make a deletion request to the Kubernetes API server, which meant that KWOK was the component that saw the error. (As I mentioned above, I was <em>expecting</em> the delete request to come from the Kubernetes Garbage Collector, which I naively thought was in controller-manager, so that&#8217;s where I spent most of my time looking).</p><p>What was the error? Well, for <code>DELETE</code> actions, the body of the webhook request populates an <code>oldObject</code> field instead of an <code>object</code> field to indicate that the object used to be there and now it&#8217;s not. Which meant that there was no <code>metadata</code> field on <code>object</code>. The solution was to duplicate the webhook for <code>DELETE</code> requests, but change the match condition to the following:</p><pre><code><code>- expression: oldObject.metadata.namespace.startsWith('virtual-')</code></code></pre><p>As soon as I made that change, everything started working. So anyways, all this is just to say, if you&#8217;re writing a webhook and things are behaving in weird ways, there are two things you need to do<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>: search your logs for error messages that seem like they are coming from really confusing places, and then think really hard about how you broke your webhook configuration so as to cause that particular log line to show up in that particular place.</p><p>As always, thanks for reading!</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Actually there are more than two parts, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Actually there are more than two ways, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Actually there are more than two types, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Actually there are more than two methods, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Actually there are more than two key things, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Actually there are more than two weird things, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>Actually there are more than two steps, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Actually there are more than two confusing bits, but these are the key ones.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Actually there are more than two things you need to do, but these are the key ones.</p></div></div>]]></content:encoded></item><item><title><![CDATA[SimArena: A reinforcement learning arena for training autonomous Kubernetes AI agents]]></title><description><![CDATA[Happy June, everybody!]]></description><link>https://blog.appliedcomputing.io/p/simarena-a-reinforcement-learning</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/simarena-a-reinforcement-learning</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Mon, 08 Jun 2026 19:30:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xHTh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xHTh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xHTh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 424w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 848w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 1272w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xHTh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png" width="670" height="440" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:440,&quot;width&quot;:670,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126137,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/201195907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xHTh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 424w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 848w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 1272w, https://substackcdn.com/image/fetch/$s_!xHTh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb92ad09b-f863-47ae-a6a1-ad0d4bec5d1c_670x440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Happy June, everybody! In this post, we&#8217;re going to take a quick detour from our <a href="https://blog.appliedcomputing.io/p/acrl-is-turning-3-and-also-drmorr">strategic plan</a> to do a recap of the second <a href="https://www.hmc.edu/clinic/">clinic project</a> that ACRL has sponsored with Harvey Mudd College (HMC). Just as a quick reminder, one of ACRL&#8217;s goals is to promote and facilitate public, open-source research and development, and the HMC clinic program is a really fantastic way to facilitate this. At HMC, every graduating senior has a requirement to participate in either a thesis project (which is a 1-1 guided research project with a faculty advisor, very similar to the experience you might get in graduate school), or a clinic project, where a team of seniors work together with a faculty advisor and sponsoring company on a project of interest to the sponsor.</p><p>Both projects that ACRL has sponsored have been SimKube-related (of course); <a href="https://blog.appliedcomputing.io/p/contraction-hierarchies-hmc-clinic?utm_source=publication-search">last year&#8217;s project</a> tackled a data-generation project, specifically trying to address the question of &#8220;How can we bootstrap a small amount of SimKube <a href="http://localhost:8000/posts/2025-06-09-anatomy-of-a-trace/">trace data</a> into a large amount of data?&#8221; One potential use case for this would be as training data for an autonomous Kubernetes AI agent. <em>This</em> year&#8217;s project tackled a complementary question, namely: &#8220;Given that we have a whole bunch of training data, how can we actually build an interface or environment for an agent to learn from it?&#8221;</p><p>Answers to both of these questions are necessary (but probably not sufficient!) for developing some machine learning model that can actually operate (or even diagnose!) complex distributed systems like Kubernetes.</p><h2>The training arena theory</h2><p>In the AI/ML space, there are lots of &#8220;arenas&#8221; or &#8220;gymnasiums&#8221; that people use as a sandbox for agents to perform learning and evaluation tasks. The idea is that you provide your reinforcement learning (RL) agent a carefully-controlled environment and a goal, and through many, many training iterations, the agent eventually learns how to accomplish the goal; and if you&#8217;re exceptionally lucky, maybe it can also generalize what it learns. There are a number of popular arenas for various video games, including <a href="https://minedojo.org/">MineDojo</a> for MineCraft and <a href="https://ai.meta.com/blog/minihack-a-new-sandbox-for-open-ended-reinforcement-learning/">MiniHack</a> for NetHack<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>And, since obviously Kubernetes is just a giant video game, this concept translates naturally.</p><p>So this was the project that I tasked the clinic team with: use SimKube to build a training arena for Kubernetes RL. Just like year, this was a very open-ended &#8220;research-oriented&#8221; project; there aren&#8217;t any right answers, and I was very up-front with the team that if we got to the end of the project and the outcome was &#8220;this is a dumb idea and here&#8217;s why,&#8221; that would have been a success in my book.</p><p>Fortunately that&#8217;s not what happened!</p><h2>To go big, you gotta start small</h2><p>Distributed systems generally (and Kubernetes specifically) are enormously complex<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, and it turns out that machine learning isn&#8217;t easy either<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. I was able to provide a lot of expertise and guidance on Kubernetes and distributed systems, but I do not have the ML expertise that was needed for this project<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>; fortunately, the <a href="https://sites.google.com/g.hmc.edu/erinjtalvitie">faculty advisor</a> that we were working with is an expert in reinforcement learning and machine learning, and was able to provide the needed expertise for this side of the project.</p><p>The first challenge that the team tackled was how to limit the scope of the project to something tractable in a 9-month period. There was a lot of excitement on the team about actually training an AI agent for Kubernetes, but this was very quickly ruled out as &#8220;too ambitious&#8221;, both from a &#8220;time&#8221; perspective and also from a &#8220;we&#8217;ve got no GPUs and no money to buy them&#8221; perspective. Instead, after a period of intense literature review and research/brainstorming, the team narrowed in on a much more tightly-scoped project: given an (extremely) small action space and a well-defined goal, can we build a set of APIs that <em>someone else</em> could use to train a Kubernetes agent on top of SimKube?</p><p>The team spent the bulk of the fall semester fleshing out this concept; the three main questions they needed to answer were &#8220;What are the inputs to the agent?&#8221;, &#8220;What does the agent output?&#8221;, and &#8220;How does the agent know it&#8217;s doing a good job?&#8221; To keep the project tractable, the team settled on the following answers:</p><ol><li><p>The input to the agent&#8212;e.g., what it can observe about the Kubernetes cluster&#8212;is just &#8220;are the desired pods healthy?&#8221; and &#8220;what are the resource requests and replica counts for each pod?&#8221; This choice of inputs naturally restricted us to &#8220;autoscaling-like&#8221; behaviours, which not coincidentally, is also what SimKube is really good at!</p></li><li><p>Once the agent has collected its input and made a decision, it outputs a Simkube <a href="http://localhost:8000/posts/2025-06-09-anatomy-of-a-trace/">trace file</a>. The modifications that the agent is allowed to make to the trace is extremely small; it can adjust pod replica counts and CPU/memory resource requests.</p></li><li><p>The agent has &#8220;succeeded&#8221; at its task when all of the pods in the cluster are healthy (aka, they have entered the &#8220;Running&#8221; phase at the end of the simulation).</p></li></ol><p>This is obviously a <em>hugely</em> simplified version of the actual problem, but I think it was a great place to start: it was simple enough to reason about in a short period of time, but also complex enough that we were able to uncover some potential problems with the approach. And, it&#8217;s obviously extensible: if you want to make the problem harder, you can either expand the observation space, the action space, or the target metric.</p><p>By the end of the first semester, the team had implemented an extremely basic POC of the Kubernetes training arena based on SimKube that was able to stitch all of these pieces together. And we&#8217;d uncovered the biggest problem we would encounter along the way: time!</p><h2>Time, time, time, see what&#8217;s become of me</h2><p>It turns out that most reinforcement learning algorithms require hundreds of thousands of training iterations before you&#8217;re able to get something &#8220;good&#8221; out of them. But SimKube runs all of its simulations in real-time! That means if your trace file lasts for an hour, your simulation has to run for an hour<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. This is.... kindof a problem. If you need 100,000 training epochs before your RL agent is able to effectively operate your Kubernetes cluster, you need to wait 100,000 hours, which is&#8230; 11 years. I&#8217;m sure in 11 years we&#8217;ll still be using Kubernetes, right? Also I hope you never need to re-train your agent.</p><p>Now, it turns out that there are some techniques in the RL community to deal with training against processes that run in real-time, and the most common one is just &#8220;run a bunch of training iterations in parallel&#8221;. So this is the approach the team took: they extended SimArena to operate in a distributed manner across multiple different machines. Every so often, the arena manager will checkpoint all of the running processes and synthesize the weights for the RL agent together. Along the way, the team got to learn all about the joys of AWS: spinning up EC2 instances, managing S3 buckets whose names are globally unique, and figuring out IAM roles. The clinic team <em>also</em> got to be the first users of ACRL&#8217;s <a href="http://localhost:8000/posts/2026-01-26-what-to-expect/">SimKube AMIs</a>, which was very exciting and validating.</p><p>So now we&#8217;ve solved the time problem: if you want your training run to take one day instead of 11 years, you just need 4200 EC2 instances! That shouldn&#8217;t be a problem for any of the big AI companies, I&#8217;m sure<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><h2>Do you even lift, bro?</h2><p>The final pieces of the project all came together within the last couple months; as the original goal of the project was to create an arena that anybody could plug their ML agent into, the team built two different hooks into SimArena.</p><p>The first was an interface based off OpenAI&#8217;s <a href="https://gymnasium.farama.org/index.html">Gymnasium</a>; if you&#8217;re unfamiliar (like I was), Gymnasium provides a standard interface for RL agents to interact with&#8230; well, just about anything. If you can make your system implement the Gymnasium API, then anybody who builds an RL agent can just plug it in and let it run. The second hook into SimArena that the team built was an <a href="https://en.wikipedia.org/wiki/Model_Context_Protocol">MCP server</a> for SimArena. MCP stands for &#8220;Model Context Protocol&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>, and was an API developed by Anthropic<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a> for allowing LLMs to interact with arbitrary systems.</p><p>The last bit of work the team did was to train a very simple RL agent based on <a href="https://en.wikipedia.org/wiki/Q-learning#Deep_Q-learning">deep Q-learning</a>, which is a standard RL learning algorithm used in a lot of different settings. I&#8217;m happy to report that not only did SimArena work, but the agent learned to make the Kubernetes pods healthy!</p><p>I&#8217;m extremely happy with the work that the team did this year, and really excited to see the results that they were able to put together; but don&#8217;t just take my word for it. The SimArena code is <a href="https://github.com/acrlabs/simarena">on GitHub</a>, and you can also read the team&#8217;s <a href="http://appliedcomputing.io/reports/ACRL_Final_Clinic_Report-26.pdf">final report</a> for more details about their work.</p><p>And lastly, I&#8217;d just like to say that I&#8217;m really excited to have had the opportunity to work with these great students and to do another clinic project at HMC, it&#8217;s been an exceptionally valuable experience for me and (I hope) also for the people I&#8217;ve gotten to work with. A huge thank you to the team and the faculty and staff at Mudd that have made this so successful!</p><p>As always, thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Obviously this is the best game on the planet.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Why yes, this <em>is</em> the opening sentence to our research paper on the subject. I think this is an extremely novel insight that nobody else has ever had before.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>TIL</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Fun fact: when I was in grad school, the two subject areas I was considering pursuing were &#8220;scheduling and optimization&#8221; and &#8220;natural language processing/machine learning&#8221;. Obviously, I picked the one that was going to be lucrative in the long term. &#129702;</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Plus a bit of extra time needed to set up and tear down the simulation environment.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>By the way, I&#8217;m willing to discuss licensing terms for SimArena to any of the big AI companies for the extremely generous pricing of &#8220;eleventy billion dollars&#8221;.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p><em>Not</em> &#8220;<a href="https://tron.fandom.com/wiki/Master_Control_Program">Master Control Program</a>&#8221;, although I think we can all agree that would be way funnier.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Just saying, SimKube is compatible with <em>both</em> APIs from the two major AI players in the space. We&#8217;re nothing if not forward-thinkers!</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[ACRL is turning 3 (and also drmorr is turning an age greater than 3)]]></title><description><![CDATA[It is kind of wild to me that ACRL was founded 3 years ago; in June of 2023 I stepped out on this crazy adventure and I had no idea where it was going to go or how long it would last, but here we are, three years later, and ACRL is still alive.]]></description><link>https://blog.appliedcomputing.io/p/acrl-is-turning-3-and-also-drmorr</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/acrl-is-turning-3-and-also-drmorr</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Sat, 30 May 2026 16:02:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!w-NC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!w-NC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!w-NC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 424w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 848w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!w-NC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg" width="614" height="416" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:416,&quot;width&quot;:614,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A grimacing monkey with the text \&quot;when people sing happy birthday to you and you just sit there like\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A grimacing monkey with the text &quot;when people sing happy birthday to you and you just sit there like&quot;" title="A grimacing monkey with the text &quot;when people sing happy birthday to you and you just sit there like&quot;" srcset="https://substackcdn.com/image/fetch/$s_!w-NC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 424w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 848w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!w-NC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06200a93-95ab-45a4-bc3c-b9186d6775ca_614x416.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Happy birthday to us! Happy birthday to us! Happy birthday to the coolest person and company in the world! Happy birthday to us!</figcaption></figure></div><p>It is kind of wild to me that <a href="https://appliedcomputing.io">ACRL</a> was founded 3 years ago; in June of 2023 I stepped out on this crazy adventure and I had no idea where it was going to go or how long it would last, but here we are, three years later, and ACRL is still alive. I feel like &#8220;company years&#8221; are kind of like &#8220;dog years&#8221;, which means I&#8217;m pretty sure ACRL is now old enough to vote<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. It also just so happens to be <em>my</em> birthday today, which I don&#8217;t think we need to discuss any further, except to say that &#8220;drmorr years&#8221; are like &#8220;inverse dog years&#8221;, which means I am <em>also</em> just now old enough to vote.</p><p>I&#8217;ve heard from a number of folks that it takes, on average, about five years for a company to go from &#8220;I don&#8217;t know if this will succeed <em>at all</em>&#8221; to something that is somewhat sustainable. Which means that ACRL is about halfway there, statistically speaking! I have been thinking a lot about what that means over the last few months, and one of the conclusions that I reached is that, while I am doing pretty well<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, I would like to be doing &#8220;pretty well faster&#8221;. The traditional thing that tech companies do when they reach this stage is to go raise a bunch of VC money, but that door is closed to me, given my core tenet of &#8220;absolutely no way in hell&#8221;. So instead, I ended up securing a private investor, who also happens to be me.</p><p>One of the goals of this blog (in additional to starting flamewars about Rust vs Go and rambling endlessly about SimKube) is to document my process for bootstrapping a company, so I thought I would spend this birthday post talking a little bit about why we decided that &#8220;right now&#8221; is the right time for such an investment, what we&#8217;re planning to do with the funds, and how we&#8217;re going to know if it&#8217;s successful.</p><h2>Supporting the next phase in ACRL&#8217;s journey</h2><p>When I started ACRL, I had a small amount of &#8220;seed money&#8221; that I took into the business. I knew, intellectually, that &#8220;you have to spend money to make money&#8221;, and I also knew that there&#8217;s just a lot of financial overhead involved in running a business. The rough goal was, if the seed money ran out before I&#8217;d gotten people to pay me, I was giving up and going to get a real job.</p><p>As evidenced by the fact that I&#8217;m still here, that seed money didn&#8217;t run out. Between the work that <a href="https://engineeringblog.yelp.com/2024/08/multi-metric-paasta.html">I did at Yelp</a> and the work that <a href="https://blog.appliedcomputing.io/p/astronomer-saving-megabux-with-sql?utm_source=publication-search">I&#8217;ve been doing at Astronomer</a>, I&#8217;ve approximately managed to 10x the initial seed money for ACRL over the last three years<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. It should be noted that I&#8217;m not attempting to brag here: this is a remarkable achievement for a small business and it needs to be acknowledged that there was (and continues to be) an extremely large amount of both luck and privilege involved in this success. Nevertheless, it <em>is</em> a success, and I&#8217;m proud of where we&#8217;ve gotten.</p><p>Unfortunately, we&#8217;ve somewhat hit the limit with my current business model; with the exception of this blog<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, all of our work is supported via consulting hours<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>, where Ian and I embed as expert consultants in client teams to help with Kubernetes. We are at a point where clients are paying us to use SimKube for them, which is&#8212;honestly&#8212;an amazing feeling, but there is a limit to how many clients we can support in this model, and that limit is, basically, two. Maybe three if we really wanted to burn ourselves out. It&#8217;s been clear to me for a while<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a> that if I want to scale beyond this, I need to change my business model.</p><p>So that&#8217;s the goal: we are trying to move a step up the &#8220;business&#8221; ladder from &#8220;selling our expertise&#8221; to &#8220;selling a product that has been developed using our expertise&#8221;. And that&#8217;s why we&#8217;ve got this investment right now: it&#8217;s going to support this next phase in our journey<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>. The goal (which I fully recognize is ambitious and not fully in my control) is to achieve <em>something like</em> a similar return on investment to what we got on ACRL&#8217;s seed money.</p><h2>Let&#8217;s write down some go-tay-jars</h2><p>Astute readers may remember a post I made a while back about <a href="https://blog.appliedcomputing.io/p/okrs-are-bullshit?utm_source=publication-search">OKRs</a>. So, I&#8217;d just like to clarify that the things I&#8217;m going to talk about in this section are <em>goals</em>. This goals are artisanal, organic, home-grown, non-GMO, and contain absolutely nothing rhyming with &#8220;Bokay Arrr&#8221; or &#8220;Jay Pee Eyes&#8221;.</p><p>Anyways, with that out of the way, it&#8217;s one thing to say that we&#8217;re going to approximately 10x our investment over the next few years, and another thing to actually do it. We need to have <em>some</em> target that we&#8217;re aiming for besides &#8220;I really hope someone gives us money!&#8221; So what are these targets? I&#8217;ve identified two:</p><ol><li><p>Have a product to demo at KubeCon: in the course of my conversations with potential clients and other interested parties over the last couple years, it&#8217;s been made extremely clear to me that SimKube (as it currently stands) is not a product, it&#8217;s a tool. It is (in my opinion) an <em>extremely</em> cool tool, but it&#8217;s also a tool that can probably only be used by myself and Ian. It&#8217;s easy to <em>explain</em> the high-level concept, but as soon as someone wants to go deeper, it&#8217;s easy to get lost in the weeds. There&#8217;s just something about showing a CLI interface and a terrible Grafana dashboard that makes people&#8217;s eyes glaze over; so if we&#8217;re going to move from &#8220;selling our expertise&#8221; to &#8220;selling a product&#8221; we need an actual, real, demo that we can show to people.</p></li><li><p>By this time next year, we should have approximately doubled our <a href="https://en.wikipedia.org/wiki/Revenue_stream#Recurring_revenue">ARR</a>: obviously if we&#8217;re going to 10x our investment over three years, we need to be making some progress on that <em>this</em> year. Just like when ACRL started, it&#8217;s going to take some time to kickstart this process, so we&#8217;re not going to get all of that 10x all at once. But approximately doubling our current ARR seems like an ambitious, but still achievable, goal. As a part of that, I&#8217;ve been spending a <em>lot</em> of time learning how to be a salesperson&#8212;and it&#8217;s kinda fun! I really did not expect to like this part of running a business<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>, nor did I expect to be particularly good at it, so it&#8217;s been a pleasant surprise to learn that I <em>do</em> kindof like it and I <em>am</em> kindof good at it. It&#8217;s definitely an untrained muscle for me though, so a big part of being able to hit this goal is going to depend on how quickly I can level up my sales skills<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p></li></ol><h2>So how do we get there?</h2><p>A couple of weeks ago, I met up with Ian and a few other &#8220;trusted&#8221; advisors in Portland to talk about our strategy for achieving these goals. It was an extremely valuable (and extremely intense) few days, with a lot of good conversation and good food<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>. A lot of the conversation revolved around refining ACRL&#8217;s target market: what types of companies we want to be talking to, and who we need to be talking to within those companies. The rest of the conversation was concerned with &#8220;what we&#8217;re trying to sell.&#8221;</p><p>You&#8217;ll be hearing <em>a lot</em> more about the strategy in the upcoming weeks and months leading up to KubeCon, but the high level theme is &#8220;Using SimKube to solve autoscaling.&#8221; Autoscaling is (still) an area that many companies struggle with, and justifiably so: it turns out to be <em>really hard</em> to do well<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>. But the general consensus from both our Portland strategy session, and conversations that I&#8217;ve had with many other people in the industry, is that ACRL is well-positioned to tackle this problem. I&#8217;ve been doing some variant of &#8220;distributed systems autoscaling&#8221; for over ten years at this point, and as one of my friends pointed out, SimKube is (at least) the third time I&#8217;ve tilted at the &#8220;distributed systems&#8221; windmill: so we have a really great combination of deep expertise, deep excitement and passion for the subject area, and lots of hard problems that still need to be solved.</p><p>Like I said: we&#8217;ll be talking a lot more about this strategy for the remainder of the year and I don&#8217;t want to give away all the magic right now, but I do want to close with a teaser: at KubeCon last year, we ran &#8220;Can U Fit In the Kube&#8221;, an <a href="https://www.youtube.com/watch?v=ZDlQzhAl8zI">epic guerilla marketing scheme</a>, which was a ton of fun and generated a lot of laughs and good vibes. We&#8217;re going to do something a little bit different at KubeCon this year: ACRL is going to host the world&#8217;s first-ever &#10024;&#65039;Autoscaling Grand Prix&#10024;&#65039;. Just like everything we do, it&#8217;s going to be fun, a little bit snarky, a little bit tongue-in-cheek, and a lot of autoscaling goodness.</p><p>We are actively looking for sponsors and partners for the Autoscaling Grand Prix, so if that&#8217;s you&#8212;or you work at a company that might be interested in this&#8212;we would <a href="https://appliedcomputing.io/contact">love to talk to you</a>.</p><p>I&#8217;m so excited that ACRL has made it to its three-year birthday, and I&#8217;m extremely excited and hopeful for what&#8217;s to come; thanks to all of you for coming along with us on this crazy journey. I&#8217;m so grateful for your support.</p><p>As always, thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Everybody in the USA please go vote while you&#8217;re still allowed to, thanks.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Particularly in this economy and job market, my goodness, what a dumpster fire.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>I&#8217;m strictly speaking in terms of revenue here; turns out paying salaries and stuff is expensive, who knew?</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Thank you to my paid subscribers! Wouldn&#8217;t you like to <a href="https://blog.appliedcomputing.io/subscribe">become one</a>?</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Long-time readers of my blog might notice that this is extremely backwards from my initial vision for funding; back in the good old days (aka 2023), I was hoping and expecting to be largely supported by federal grant money, at least for a while. I had a number of grant submissions that I&#8217;d made that, while unsuccessful, were <em>extremely close</em> to meeting the funding criteria. However, given the, uh, <a href="https://arstechnica.com/science/2026/05/the-office-of-management-and-budget-tries-again-to-cripple-us-science/">&#8220;current state&#8221;</a> of the United States government, coupled with the fact that writing grant proposals is <em>incredibly</em> time consuming, I&#8217;ve made the decision to halt pursuing that as a funding source for the time being. Maybe that will change again in the future? We&#8217;ll see.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Mostly because the ACRL board has been pounding me over the head with it for the last two years.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>I keep wanting to call it a pivot, but it&#8217;s really not a pivot. In the early days of the business, I was willing to take money from just about anybody to do just about anything; it was all about staying afloat. Once I felt &#8220;reasonable&#8221; comfortable and secure, the next step was to narrow the focus: I&#8217;m no longer willing to take money from anybody to do anything. If I can&#8217;t tie (at least a tenuous) line from the work to SimKube, I&#8217;m not really interested in doing it. And we&#8217;re now that the next phase of development: we&#8217;re still doing SimKube, we&#8217;re still going to be working extremely closely with teams who are using it, but the product is no longer &#8220;drmorr&#8221;. So from that perspective, this is not a pivot at all, but an extremely natural progression in ACRL&#8217;s growth.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Largely based on prior experiences where I was asked to sell things that I did not care about and did not believe in.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>I have a sales coach I&#8217;ve been working with who is absolutely fantastic, I cannot recommend her enough&#8212;let me know if you&#8217;d like an intro.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>Turns out Portland is a cool city! Who knew???</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p>And no, before you ask, Claude can&#8217;t help you.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Naked pods are Weird, Man]]></title><description><![CDATA[I&#8217;ve been doing some work with SimKube for a client recently and have run into a bunch of issues with SimKube fixed regarding its handling of bare Kubernetes pods.]]></description><link>https://blog.appliedcomputing.io/p/bare-pods-are-weird-man</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/bare-pods-are-weird-man</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Sun, 24 May 2026 16:02:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!w88f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!w88f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!w88f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 424w, https://substackcdn.com/image/fetch/$s_!w88f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 848w, https://substackcdn.com/image/fetch/$s_!w88f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!w88f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!w88f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg" width="1280" height="840" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:840,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!w88f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 424w, https://substackcdn.com/image/fetch/$s_!w88f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 848w, https://substackcdn.com/image/fetch/$s_!w88f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!w88f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe28b5d7c-5cc4-4193-9729-9f8546488f89_1280x840.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Artist&#8217;s rendition of naked Kubernetes pods.  By Chiswick Chap - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=143381373</figcaption></figure></div><p>I&#8217;ve been doing some work with <a href="https://simkube.dev/">SimKube</a> for a client recently and have run into a bunch of issues with SimKube fixed regarding its handling of bare Kubernetes pods. Bare pods are kindof an unusual special case for Kubernetes so I thought it might be interesting and/or instructive to see some of the problems I&#8217;ve been having and what the fixes have looked like. But before I dive into the details, let me give a quick reminder of what we&#8217;re all doing here<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Applied Computing Research Labs is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>A Quick Reminder Of What We&#8217;re All Doing Here &#8482;&#65039;</h2><p>SimKube is a &#8220;record-and-replay&#8221; simulation environment for Kubernetes that we&#8217;ve been building for the last couple of years; it works by recording a &#8220;trace&#8221; of events that occurred in a production Kubernetes environment, and then replaying those same events in a simulated environment. In the simulator, we use <a href="https://kwok.sigs.k8s.io/">KWOK</a> to mock out the application pods and the compute nodes they&#8217;re running on, while still running all of the real control plane components. This lets us simulate extremely large Kubernetes clusters on your local dev machine<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.</p><p>Under &#8220;normal&#8221; operation, the trace file collects changes over time to &#8220;higher-level&#8221; Kubernetes resources, like Deployments, CronJobs, StatefulSets, or any custom resources that you&#8217;ve installed. It then uses real controllers to manage those objects and create the actual (simulated) pod objects that they own. This way you can be sure that the controllers are running in the same way that they might in a &#8220;real&#8221; environment<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>However, in some cases, you might not want to (or might not be able to) watch the higher-level resources, but you still want to understand some properties of your production cluster (like autoscaling or scheduling behaviour). In this case, you can configure SimKube to watch &#8220;bare pods&#8221;&#8212;that is, the raw pod objects themselves, independent of their owning resources. In principle this will work just as well in SimKube; however, because it&#8217;s a bit of an edge case, it hasn&#8217;t been as well tested or supported during development, so we&#8217;ve encountered some problems in the last few weeks. What kind of problems? I&#8217;m so glad you asked!</p><h2>Bare pods problem #1: Pod Spec Template Paths</h2><p>The &#8220;replay&#8221; component of SimKube is called the SimKube driver, which is a pod that sits in your simulation Kubernetes environments; it is responsible for a) replaying all of the events in the collected trace file, and b) intercepting the controller-created pod objects to re-target them to the fake KWOK nodes. In order to apply the correct node selectors, labels, and annotations to the simulated pods, the driver needs to know what these pods &#8220;look like&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. All of the core Kubernetes resources (Deployments, etc) and <em>most</em> third-party/custom resources do this via a &#8220;pod template&#8221;, where users actually embed the pod YAML into the higher-level resource and the owning controller will use that template to &#8220;stamp out&#8221; copies of the pods it should create. The SimKube driver modifies the contents of this pod template to make sure they run in the right place, which means it needs to know &#8220;where&#8221; in the higher-level manifest that template is defined. SimKube does this through a <code>podSpectemplatePaths</code> field defined in its configuration<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>, where the path is represented in <a href="https://datatracker.ietf.org/doc/html/rfc6901">JSON pointer</a> notation. For example, the <code>podSpecTemplatePath</code> for a Deployment is</p><pre><code><code>/spec/template</code></code></pre><p>whereas for a CronJob, it is</p><pre><code><code>/spec/jobTemplate/spec/template</code></code></pre><p>But what should the <code>podSpecTemplatePath</code> for a bare pod be? It&#8217;s kindof a weird question because there&#8217;s no other controller that is &#8220;stamping out&#8221; copies of that pod, it&#8217;s just&#8230; the pod. But it turns out not to matter, because the SimKube driver can still do all the modifications it needs in order to run the sim. So, what is it?</p><p>If you answered <code>"/"</code>, you would be incorrect, but you&#8217;d be wrong in good company because <a href="https://github.com/acrlabs/simkube/pull/240">both Ian and I</a> thought that&#8217;s what it should be at first. However, it turns out that SimKube prepends the <code>podSpecTemplatePath</code> value to a JSON pointer suffix in several places, and using <code>"/"</code> for bare pods results in a double-slash, which is incorrect and caused everything to crash:</p><pre><code><code>//metadata/labels  # This is not correct</code></code></pre><p>The <a href="https://github.com/acrlabs/simkube/pull/243/changes">fix for this</a> was extremely simple.</p><h2>Bare pods problem #2: The pod&#8217;s already been scheduled</h2><p>The second &#8220;bare pods&#8221; problem we ran into was around scheduling; the normal flow of operations in Kubernetes is</p><ol><li><p>Something (usually a human operator) creates a resource (like a Deployment).</p></li><li><p>The Kubernetes controller that owns that resource (e.g., <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/">controller-manager</a> does some stuff that eventually results in pods getting created.</p></li><li><p>The <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/">Kubernetes scheduler</a> tries to assign that pod to a node, or report that no node is available.</p></li><li><p>If the pod was scheduled to a node, the <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/">kubelet</a> starts the containers in the pod and kicks off the application code.</p></li></ol><p>The part we&#8217;re interested in here is in step 3; see, in the pod specification, there is a <code>nodeName</code> field which indicates what node the pod has been assigned to. However, this field is immutable; once the pod object has been created, you can&#8217;t change it later. Instead, you have to use the bind API to bind the pod to the node (I wrote an <a href="https://blog.appliedcomputing.io/p/writing-a-kubernetes-scheduler-in?utm_source=publication-search">entire blog post about this wayyyyyyyy long ago in the early days of ACRL</a>).</p><p>Again, this is all fine and dandy normally, but if your trace file contains bare pods, it is likely that these pods have already been scheduled to some node in the cluster that the trace came from, which means that the <code>nodeName</code> field will be set to some node that doesn&#8217;t even exist in your simulation environment! This clearly will cause problems; the fix here is again <a href="https://github.com/acrlabs/simkube/pull/242">extremely simple</a>.</p><h2>Bare pods problem #3: My pods are running forever!</h2><p>My initial thinking when tracking bare pods is that they work just like any other Kubernetes resource: we track when they&#8217;re created and we track when they go away, and everything else will work as normal. Pods are weird, though, because they can <em>finish</em> without being <em>deleted</em>. This is normal in the case of Jobs and CronJobs; it turns out to <em>also</em> be normal in the case of tracking bare pods.</p><p>Fortunately, SimKube has already built in primitives to handle this for Jobs and CronJobs, using the lifecycle annotation capabilities of KWOK. Basically, in addition to watching changes to &#8220;higher-level&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a> Kubernetes resources, the SimKube tracer <em>also</em> can be configured to watch for pod lifecycle events&#8212;e.g., when the pod containers start and finish. It records all this information inside the trace so these pod lifecycles can be replayed. In the sim, whenever we detect matching pod lifecycle data in the trace, we set the following annotations on the simulated pods:</p><pre><code><code>simkube.kwok.io/stage-complete=true
simkube.kwok.io/stage-complete-time="2026-06-01 11:00:00"</code></code></pre><p>We then create a <a href="https://kwok.sigs.k8s.io/docs/user/stages-configuration/">KWOK stage</a> such that when the indicated completion time has passed, KWOK will move the pod from the &#8220;Running&#8221; phase to the &#8220;Succeeded&#8221; phase. Cool! We should just be able to use this for bare pods, right?</p><p>Turns out, yes you can! Say it with me now: &#8220;The fix is extremely simple.&#8221;</p><p>HAHAHAHAHAHAHA I tricked you good. The <a href="https://github.com/acrlabs/simkube/pull/251">fix</a>, while being relatively small, isn&#8217;t simple.</p><p>There are two problems that you run into: the first is that, in order to limit the amount of data in a trace file, we only record the pod lifecycle data for pods that are owned by a resource that the tracer is tracking. For bare pods, there is no such owner! It turns out that we can just set the pod to be its own owner inside the trace file; it&#8217;s a little weird, it&#8217;s arguable whether this is correct or not, but it does work without needing to do a bunch of special-casing.</p><p>The second problem is that you need to know whether the pod finished running <em>before</em> the simulation started or not. Remember: the pod <em>object</em> can still exist in etcd, long after all of the containers in the pod have completed running. If you don&#8217;t know when the containers stopped relative to the start of the simulation, it&#8217;s going to throw off all your analysis<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>.</p><p>Unfortunately there&#8217;s no good way to track this without adding some kind of new field into the trace file, which I was reticent to do. It&#8217;s not impossible, and I made some changes with <a href="https://blog.appliedcomputing.io/p/the-bug-that-led-to-simkube-20?utm_source=publication-search">SimKube 2.0</a> so that I can make these changes in a backwards-compatible way, but I still wanted to avoid it if possible. Then I came up with a ~dirty hack~ genius solution: instead of modifying the trace file format, I can <em>instead</em> just modify the Kubernetes resources in the trace. Now the tracer inserts two annotations on every object it tracks indicating the original creation time and deletion time of the resource.</p><p><em>Most</em> of the time, this information is just ignored; however, if the resource in the trace is a bare pod, the simulation driver will see that these values are set on the pod, and it can then use that to calculate the remaining lifecycle time for that pod object, relative to the starting timestamp on the simulation.</p><p>I have no idea if that solution is going to bite me in the future, but for now it seems to work OK.</p><p>Also, we&#8217;re writing some tests to check for regressions in the bare pod behaviour, cuz those things are *#%&amp;ing weird, man.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Hi new readers from Reddit!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Or anywhere else you might want, like, say, <a href="https://blog.appliedcomputing.io/p/boy-ami-glad-to-see-you">in your CI pipeline</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Have I used enough &#8220;air&#8221; &#8220;quotes&#8221; for you in this post so far? Don&#8217;t worry, I&#8217;m sure there&#8217;ll be more!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Some people have complained about the number of semicolons and footnotes that I use in my blog posts, so I thought I&#8217;d branch out and start adding more quotation marks.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>You might be wondering why this is plural; some third-party Kubernetes resources create multiple different types of pods, which mean they have multiple different pod templates defined in different locations in the YAML.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>There&#8217;s those darned quotes again.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>Indeed, I discovered this problem initially because I was comparing simulation results to the actual cluster, and discovered that there were more than twice as many running pods in the simulation as there &#8220;should have been&#8221;</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Boom, quotes in footnotes; now I just need a semicolon&#8212;and maybe an em-dash.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>This is how you know I&#8217;m not writing this with AI.  If I ever write a post without footnotes, you&#8217;ll know that I&#8217;ve sold out and just asked ChatGPT to write something for me.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>Sadly, substack doesn&#8217;t let you put footnotes in footnotes, which is incredibly annoying.</p></div></div>]]></content:encoded></item><item><title><![CDATA[All the ways to mock your Rust code]]></title><description><![CDATA[Ok, so, suppose you&#8217;ve written a Kubernetes controller, you did it in Rust, and then you realized that &#8220;Hey, when this thing breaks it&#8217;s hard to understand what&#8217;s going on&#8221;. So, you decide to emit some Kubernetes events that give a little more context as to what your controller is doing and what steps it&#8217;s taken. You run all your tests]]></description><link>https://blog.appliedcomputing.io/p/all-the-ways-to-mock-your-rust-code</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/all-the-ways-to-mock-your-rust-code</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 01 May 2026 20:01:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9UHJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9UHJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9UHJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 424w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 848w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 1272w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9UHJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png" width="936" height="713" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:713,&quot;width&quot;:936,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:461447,&quot;alt&quot;:&quot;Nelson from the Simpsons pointing and saying \&quot;HA HA!\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/196141787?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Nelson from the Simpsons pointing and saying &quot;HA HA!&quot;" title="Nelson from the Simpsons pointing and saying &quot;HA HA!&quot;" srcset="https://substackcdn.com/image/fetch/$s_!9UHJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 424w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 848w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 1272w, https://substackcdn.com/image/fetch/$s_!9UHJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f6dc60e-69f8-4fc8-bc3a-9af82909b561_936x713.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Ok, so, suppose you&#8217;ve written a Kubernetes controller, you did it in Rust<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, and then you realized that &#8220;Hey, when this thing breaks it&#8217;s hard to understand what&#8217;s going on&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. So, you decide to emit some Kubernetes events that give a little more context as to what your controller is doing and what steps it&#8217;s taken. You run all your tests<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> and expect them to all pass<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, because you didn&#8217;t change any functionality, you&#8217;re just emitting some events, this is basically just like adding some log lines, right? And all the tests fail. Because you&#8217;re not just adding log lines, you&#8217;re making a network request to the Kubernetes apiserver, and in your unit tests there&#8217;s no apiserver listening. What do?????</p><p>OK so this post is not actually hypothetical, as you maybe figured out by now. This is exactly what I tried to do with <a href="https://simkube.dev/">SimKube</a> recently and I spent WAY TOO LONG trying to figure out the ABSOLUTE BEST way to do this and still make all my tests pass. This post also touches on a particular pet peeve of mine around programming practices, which is namely: &#8220;DON&#8217;T MAKE ME MAKE MY PRODUCTION CODE WORSE JUST TO MAKE IT EASIER TO TEST, COMMA JERK&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. So I figured <em>somebody</em> ought to benefit from all the time I wasted on this, so here it is: all the ways to mock a network call in Rust.</p><h2>The setup</h2><p>The <a href="https://kube.rs/">kube-rs</a> Rust library provides a built-in interface for writing controllers; the main thing you have to provide is a <code>reconcile</code> function which gets called whenever the controller needs to &#8220;do something&#8221;. The signature for <code>reconcile</code> looks like this:</p><pre><code><code>pub async fn reconcile&lt;T: kube::Resource&gt;(
    object: Arc&lt;T&gt;, global_ctx: Arc&lt;GlobalContext&gt;) -&gt; Result&lt;Action, Error&gt;</code></code></pre><p>The first argument is the Kubernetes object that changed, and the second argument is a user-defined &#8220;context&#8221; that includes additional &#8220;stuff&#8221; that the reconciler needs to be able to operate successfully. For this post we&#8217;re concerned with that context argument, which looks like this:</p><pre><code><code>struct GlobalContext {
    client: kube::Client,
    recorder: SkEventRecorder,
}</code></code></pre><p>The first field is the client we use to talk to the Kubernetes API server (we don&#8217;t want to recreate that on every reconcile for performance reasons). The second field is our event recorder, and you don&#8217;t want to recreate that on every reconcile because it loses its memory of &#8220;what events I&#8217;ve already sent&#8221;, so instead of nice aggregated events (&#8220;this event happened 57 times in the last 4 seconds&#8221;), you get 57 separate events in 4 seconds.</p><p>The event recorder struct is just a wrapper around <code>kube::runtime::events::Recorder</code> from the <code>kube-rs</code> library, which includes a few helper functions for generating and sending the events. This is a simplified version of the real thing:</p><pre><code><code>use kube::runtime::events;

struct SkEventRecorder {
    recorder: events::Recorder,
}

impl SkEventRecorder {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }

    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
}</code></code></pre><p>And there you can see the network calls we&#8217;re making: every <code>publish</code> sends the event off to the apiserver and waits for a response. How do we make this code testable? Let me count the ways.</p><h2>Method 1: Boxed Trait</h2><p>This first approach is the most common, and it uses <a href="https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)">interfaces</a> and <a href="https://en.wikipedia.org/wiki/Dependency_injection">dependency injection</a> to provide a real event recorder in your production code and a fake event recorder for your tests. We&#8217;re going to change our code to look like this:</p><pre><code><code>#[async_trait]
pub trait EventRecordable {
    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt;;
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt;;
}

struct SkEventRecorder {
    recorder: events::Recorder,
}

impl SkEventRecorder {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }
}

impl EventRecordable for SkEventRecorder {
    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
}

struct GlobalContext {
    client: kube::Client,
    recorder: Arc&lt;dyn EventRecordable&gt;,
}</code></code></pre><p>This pattern is familiar to you if you&#8217;ve ever written any Go code; literally everything in Go implements an interface simply because it&#8217;s the <em>only way</em> to make anything testable. Given the above, you can then create a <code>MockSkEventRecorder</code> object in your testing code that implements the <code>EventRecordable</code> trait<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>, and pass that in to the <code>GlobalContext</code> object in your tests.</p><p>There is actually a slight simplification we can make to this code; the <em>only</em> thing that needs to be mocked is the recorder itself, we can use the same <code>send_event_X</code> methods for both implementations:</p><pre><code><code>#[async_trait]
trait EventRecordable {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt;;
}

struct EventRecorder {
    recorder: events::Recorder,
}

impl EventRecordable for EventRecorder {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt; {
        Ok(self.recorder.publish(event_a).await?)
    }
}

struct SkEventRecorder {
    recorder: Arc&lt;impl EventRecordable&gt;,
}

impl SkEventRecorder {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }

    #[cfg(test)]
    pub fn new_with_mock() -&gt; SkEventRecorder { 
        /* initialize a new recorder with a mock sender */ 
    }

    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
}

struct GlobalContext {
    client: kube::Client,
    recorder: SkEventRecorder,
}</code></code></pre><p>This second variant is <em>slightly</em> nicer in that it reduces the number of abstractions that a dev reading the reconcile function has to look at, but it&#8217;s functionally equivalent to the first version. This pattern is <em>deeply</em> frustrating to me for three reasons:</p><ol><li><p>You have to use the <code>async_trait</code> crate, because you can&#8217;t have <code>async</code> traits in standard Rust. It&#8217;s fine, the crate is fine, it works well, it&#8217;s literally just an extra import and a proc macro annotation, but it annoys me that I have to use it.</p></li><li><p>I&#8217;ve now added a bunch of extra boilerplate in my code that isn&#8217;t really necessary. There&#8217;s <em>no reason</em> to use dependency injection here aside from making it easier to test. Someone else who comes along to read the code has an additional layer of abstraction that they have to parse through to understand what&#8217;s going on, and I&#8217;d like to have fewer abstraction layers, not more.</p></li><li><p>Most damning, this code now has a performance impact on what&#8217;s running in production! We have two pointer indirections where before we had zero: the Rust compiler doesn&#8217;t know how big an <code>impl EventRecordable</code> is, so you <em>must</em> create it on the heap, and because this is highly asynchronous code, you have to use an <code>Arc</code> instead of a <code>Box</code>, so now we have a pointer lookup <em>and</em> a reference counter to care about. Not only that, we&#8217;re now using <a href="https://en.wikipedia.org/wiki/Dynamic_dispatch">dynamic dispatch</a> (essentially a vtable, aka another pointer lookup) to figure out where the function definitions are for the event recorder.</p></li></ol><p>Now maybe if you&#8217;re writing Go code which already does a bunch of vaguely non-performant things, you don&#8217;t care, but this is Rust, dammit! You&#8217;re <em>supposed</em> to prematurely hyper-optimize everything! Isn&#8217;t there a better way?</p><h2>Method 2: Generics</h2><p>We can address concern number 3 by converting from dynamic dispatch to <a href="https://en.wikipedia.org/wiki/Static_dispatch">static dispatch</a>, which gets rid of the performance impact. The way you do that in Rust is with generic type parameters, and it looks like this:</p><pre><code><code>#[async_trait]
pub trait EventRecordable {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt;;
}

struct EventRecorder {
    recorder: events::Recorder,
}

impl EventRecordable for EventRecorder {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt; {
        Ok(self.recorder.publish(event_a).await?)
    }
}

struct SkEventRecorder&lt;T: EventRecordable&gt; {
    recorder: T,
}

impl&lt;T: EventRecordable&gt; SkEventRecorder&lt;T&gt; {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }

    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
}

struct GlobalContext&lt;T: EventRecordable&gt; {
    client: kube::Client,
    recorder: SkEventRecorder&lt;T&gt;,
}</code></code></pre><p>This code has transformed a runtime cost into a compile-time cost, which is great! The Rust compiler will &#8220;monomorphize&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a> this into the right thing when we build our binary or run the tests; no heap allocation, no vtable lookup. This is great! The only problem? This is ugly as $#&amp;%. That stupid template parameter has wormed its way all the way up into my top-level <code>GlobalContext</code> struct! Now someone who comes along to read this code has a <em>lot</em> of stuff they have to parse through to figure out what&#8217;s going on. This is unacceptable! What else is there?</p><h2>Method 3: Compile-time feature flags (take 1)</h2><p>You know what, this whole trait thing is pretty terrible, what if we just&#8230; didn&#8217;t do that?</p><pre><code><code>struct SkEventRecorder {
    recorder: events::Recorder,
}

impl SkEventRecorder {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }
}

#[cfg(not(test))]
impl SkEventRecorder {
    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.publish(event_a).await?)
    }
}

#[cfg(test)]
impl SkEventRecorder {
    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; { Ok(()) }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; { Ok(()) }
}

struct GlobalContext {
    client: kube::Client,
    recorder: SkEventRecorder,
}</code></code></pre><p>This is kinda cute. We&#8217;re using Rust compile-time directives to say &#8220;If you&#8217;re running this under test, just always return immediately, but if you&#8217;re a real binary send an event&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>. There are two problems here: one is that if, at some point in the future I decide I need more fine-grained tests for the event recorder, I can&#8217;t without jumping through a whole bunch&#8217;o hoops. The second is that in the real code, the event recorder and the context are in different crates, and the <code>test</code> configuration parameter doesn&#8217;t get passed through to crate dependencies&#8212;which makes sense, but means I now have to introduce a new feature into my code and juggle that in various <code>Cargo.toml</code>s. Gross.</p><h2>Method 4: Compile-time feature flags (take 2)</h2><p>We can actually combine the two approaches above, and get some godawful frankentrait code:</p><pre><code><code>#[async_trait]
pub trait EventRecordable {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt;;
}

struct EventRecorder {
    recorder: events::Recorder,
}

impl EventRecordable for EventRecorder {
    async fn send_event(&amp;self, event: &amp;events::Event) -&gt; Result&lt;(), Error&gt; {
        Ok(self.recorder.publish(event_a).await?)
    }
}

struct SkEventRecorder {
    #[cfg(not(test))]
    recorder: EventRecorder,

    #[cfg(test)]
    recorder: MockEventRecorder
}

impl SkEventRecorder {
    pub fn new() -&gt; SkEventRecorder { /* initialize a new recorder */ }

    pub async fn send_event_A(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_a = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
    pub async fn send_event_B(&amp;self) -&gt; Result&lt;(), Error&gt; {
        let event_b = events::Event::new( /* make an event */ );
        Ok(self.recorder.send_event(event_a).await?)
    }
}

struct GlobalContext {
    client: kube::Client,
    recorder: SkEventRecorder,
}</code></code></pre><p>This has all the drawbacks of approach 3, with the added benefit that it looks really really dumb.</p><h2>Method 5: The actor framework</h2><p>Ok, so this is all dumb and frustrating, let&#8217;s take a step back and figure out what we&#8217;re actually trying to do here<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. We want to: a) send events, b) over a network, c) and test it. Maybe the problem is that we haven&#8217;t made our code convoluted <em>enough</em>!</p><p>I&#8217;m not going to write it all out here, but the other &#8220;standard&#8221; approach to solving this problem is using something called the <a href="https://en.wikipedia.org/wiki/Actor_model">actor model</a>. The basic idea is that the bits of code in your program are &#8220;actors&#8221; and the only way that actors can communicate with each other is by sending messages, typically over some type of queue. So, in this model, we would re-write our reconcile function to just submit events to a queue, and then we would have some other bit of code on the other end that is asynchronously listening to that queue, and forwarding those events along to the Kubernetes apiserver over the network connection, which you can imagine is just another type of queue<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>.</p><p>And actually, the actor pattern is a pretty good one! It has a lot of use cases, especially for more complicated interactions, but remember: we&#8217;re just trying to write some fancy log lines here! Come on, what are we even doing, I&#8217;m not spinning up a whole queue and separate async reconcile loop to handle the log lines from my primary reconcile loop. This is insanity!</p><h2>Method 6: Fake the apiserver</h2><p>Ok, the core problem that we&#8217;re running into here is that <em>all</em> of these approaches require changing your production code from something simple and straightforward (&#8220;do a thing, record that you did the thing, do the next thing&#8221;) into an unholy abomination just to make it easier to test. What if we just&#8230; didn&#8217;t do any of that? Instead, we could leave our code the same and just spin up a fake Kubernetes apiserver in our test harness to listen and respond to events. Then we can test real interactions with a real apiserver, with only the very tiny problem that we&#8217;ve now had to create an entire Kubernetes apiserver so that our unit tests pass, <em>and</em> we&#8217;ve introduced a network call into our tests, and networks are famously incredibly robust and never drop packets and absolutely will not create flaky tests all willy-nilly, nosiree.</p><h2>Method 7: There is no method 7</h2><p>So that&#8217;s it! That&#8217;s all the ways I know of to mock out your Rust code for the purposes of testing. None of them are satisfying. All of them feel gross in various ways. And yet, I don&#8217;t think there&#8217;s a better way to do it? The problem with statically-typed, compiled languages like Rust (and to a lesser extent, Go), is that you have to tell the compiler that you have a Real Type that is going to do Real Type Things at compile time, and the unfortunately reality is that makes testing harder<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>.</p><p>What approach did I end up taking? Well, after venting about all this for a while to Claude, the robot finally told me: &#8220;What are you doing, you dumbass<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>? The cost of a network request is orders of magnitude higher than the cost of a couple of pointer lookups. Just stick it in an <code>Arc</code> and call it a day.&#8221;</p><p>So that&#8217;s <a href="https://github.com/acrlabs/simkube/pull/237">what I did</a>. I&#8217;m kindof lowkey upset that the stochastic labubu was right about this, but, well, I guess we can&#8217;t win them all.</p><p>Anyways, thanks for reading! Tune in next time for more SimKube stuff.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Naturally.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Purely hypothetically speaking, of course. I would never write a broken Kubernetes controller.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Because you do have tests, right? No madlad would write a Kubernetes controller without tests.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>This is how you <em>know</em> this scenario is completely unrealistic, nobody ever expects all their tests to pass.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>See also: the Python debugger. Boy howdy does it really grind my gears every time I have to add an <code>import pdb; pdb.set_trace()</code> line to my Python code. Just let me start the debugger from the command line FFS!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Or you can use the <a href="https://docs.rs/mockall/latest/mockall/">mockall</a> crate which does this for you automagically.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>This has a precise definition but for all intents and purposes it means &#8220;vomit out a bunch of generated code for each type <code>T</code> that gets passed in&#8221;.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>The aforementioned <a href="https://docs.rs/mockall/latest/mockall/">mockall</a> crate actually will do this for you as well, and also give you a mock struct that lets you perform some assertions on it, but it&#8217;s&#8230; not super ergonomic to use.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>&#8220;What Problem Are You Solving?, or, as my friend likes to remind me with a really silly acronym, &#8220;WPAYS?&#8221;</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>A real &#8220;Yo dawg, I heard you like queues&#8221; moment here.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p>When I was venting to my WPAYS friend about this, he replied, extremely condescendingly, &#8220;Tell me you do a ton of dynamic programming without telling me what language you use&#8221;, and OK fair, this problem is much easier to solve in Python. I can own that.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p>Verbatim quote.</p></div></div>]]></content:encoded></item><item><title><![CDATA[SRECon 2026 Recap]]></title><description><![CDATA[Last month I had the pleasure of going to SRECon 2026 Americas in Seattle; I realized that I posted the transcript of my talk but I didn&#8217;t actually do a recap of the conference itself, so I&#8217;m going to use this post to do so.]]></description><link>https://blog.appliedcomputing.io/p/srecon-2026-recap</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/srecon-2026-recap</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 24 Apr 2026 23:30:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!M8wv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9abdbcc8-dc09-4e12-8fc3-348cb9c2691e_518x518.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tmQE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 424w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 848w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 1272w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tmQE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg" width="300" height="105" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:105,&quot;width&quot;:300,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tmQE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 424w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 848w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 1272w, https://substackcdn.com/image/fetch/$s_!tmQE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a770a99-9331-491b-9f75-224728d38bdc_500x175.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></div></figure></div><p>Last month I had the pleasure of going to <a href="https://www.usenix.org/conference/srecon26americas">SRECon 2026 Americas</a> in Seattle; I realized that I posted <a href="http://localhost:8000/posts/2026-04-06-srecon/">the transcript of </a><em><a href="http://localhost:8000/posts/2026-04-06-srecon/">my</a></em><a href="https://blog.appliedcomputing.io/p/stop-reading-changelogs-safer-kubernetes"> talk</a> but I didn&#8217;t actually do a recap of the conference itself, so I&#8217;m going to use this post to do so. In short: it was a wonderful conference, and I&#8217;m definitely hoping to attend again next year!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">ACRL is trying to increase our subscription revenue to $5000/year to cover expenses to attend conferences like SRECon.  If you find these recaps interesting, consider signing up below!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>The highlights</h2><p>A loooooong time ago in a past life, I had the opportunity to go to LISA<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, which was another Usenix conference focused on sysadmin/devops-type content. Unfortunately, LISA stopped being a thing; the last LISA was in 2021 as a virtual event, and the last in-person LISA was in 2019 in Portland. I had several folks at SRECon this year tell me that SRECon has become kindof the &#8220;spiritual successor&#8221; to LISA, and I can definitely say that feels true based on my experience! Both LISA and SRECon were/are smaller conferences<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> with a really strong focus on high-quality talks and giving lots of space for people to meet and interact<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. While some of the talks made me grumpy with how much Ayyy Eyye was in them, there wasn&#8217;t a single talk that was <em>bad</em>. All the presenters that I saw gave very highly polished presentations about really interesting topics.</p><p>But, as I think is the case for a lot of folks, the highlight of the conference was the hallway track. There&#8217;s a lot of space and expectation that you can just walk up to somebody and start a conversation, and then they will see somebody else passing by that they know professionally and be like &#8220;Hey so-and-so, we&#8217;re talking about X, wanna join us,&#8221; and suddenly you&#8217;re in a conversation with 10 really smart people you&#8217;ve never met before. AND, the thing that&#8217;s cool about smaller conferences is that it&#8217;s <em>highly likely</em> you will see those same people again later, so you can go back to them and say &#8220;Hey, I was thinking about this thing you said yesterday&#8230;&#8221; and start the whole process over again, and then a total stranger shoves a clipboard in your face and says, &#8220;Hey you seem like you have a lot to say, do you want to be on a podcast, we&#8217;re recording it in that room, sign up for a time slot here!&#8221; and then all of sudden you&#8217;re recording a ten-minute podcast episode with some really influential folks from Google<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. It&#8217;s a really refreshing change from something like KubeCon, where the conference is so big and broad that it can be hard to find &#8220;your crowd&#8221;, so to speak.</p><p>The other thing that SRECon did that I&#8217;ve not seen anywhere else is, in addition to two tracks of normal conference talks, every timeslot had a &#8220;discussion session&#8221; on a particular topic. These sessions typically had one or two facilitators, who might have some light rules/guidelines/suggestions of things to talk about, but by-and-large it was a safe space<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a> to have an hour-and-a-half focused conversation with a group of ten or so strangers on a dedicated topic. I went to a really interesting discussion on mental health in software engineering, and part of a discussion around AI and SRE/devops<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><h2>The talks</h2><p>Like I said above, all of the talks I went to were really high quality, but I&#8217;ll just put a few of my favorites here (also, the SRECon 2026 <a href="https://www.youtube.com/playlist?list=PLbRoZ5Rrl5lfnl2qlgWH48uttpkVF4-4R">YouTube playlist</a> is available, so you can watch all of them online!)</p><ul><li><p><a href="https://www.youtube.com/watch?v=aYq9a4qUszs">The WTF Problem: Developer Experience as a Reliability Property</a>: This talk by Nicole Forsgren at Google<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>, was the second plenary talk of the conference, and I loved the premise: your developer tooling is impacting your ability to quickly respond to incidents. Nicole started off with a series of questions: How many have been paged for something your tooling should have caught? How many have a runbook that you don&#8217;t trust? How many have context-switched out of an incident and into &#8220;how do my debugging tools work?&#8221; (Nearly) every single person in the room raised their hand for every question. She then segued into a new thing we can measure, which she called &#8220;Mean Time to WTF (MTTW)&#8221;&#8212;basically, how long does it take for an engineer on your team to go from &#8220;I got paged&#8221; to &#8220;I understand what is happening here?&#8221; Much of the friction that impacts MTTW is about developer tooling that doesn&#8217;t work or is out of date, and if you invest in improving in that you will improve your reliability posture. I thought this was a great talk, and it really reinforced my decision at ACRL to &#8220;act like a big company&#8221; with respect to tooling, automation, and the like: it would be &#8220;easier&#8221; to just click buttons on the AWS console to get the resources and things that I need, but by spending time getting the automation right <em>now</em> when we&#8217;re small means (hopefully) we have less pain in the future as we grow.</p></li><li><p><a href="https://www.youtube.com/watch?v=OFMR8BYKvCU">Infinity is not a strategy: right-sizing the cloud</a>: This talk was by Praval Panwar from Microsoft. The talk was largely a discussion about the tensions between cost and reliability, which any of my long-time readers will know is something that I harp on a lot. You can have 100% reliability by scaling to infinity, but that strategy is neither a) feasible or b) cost-effective. Praval spent the remainder of the talk discussing strategies you can use to manage this tension: things like oversubscription, load forecasting, load-shedding, error budgets, etc. etc. etc. I really liked in this talk that he got into some of the math and statistics behind these techniques. At its core, risk is about &#8220;quantifying uncertainty&#8221;, which sometimes I think devops people forget means &#8220;doing math&#8221;. Also he mentioned mixed-integer programming, which is near and dear to my PhD research, so I was just extra happy to be in this talk.</p></li><li><p><a href="https://www.youtube.com/watch?v=5z3NJGVj0AE">Keeping a Hypervisor Fleet Up to Date with Minimal Customer Disruption</a>: This talk, by Atalay Kutlay at Akamai, was really fun because (again) it&#8217;s a really cool application of mathematical optimization to distributed systems. Atalay described how they use mixed-integer programming<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a> to keep their fleet of hypervisors (read: virtual machines) up to date. The problem, more or less, is whenever you need to update a VM, you have to a) kick all the workloads off, and b) reschedule them somewhere else, before you can update the VM. You want to do this in a &#8220;fair&#8221; manner, for some definition of fair, and it turns out that you can just write this all down in a big linear program and solve it to get your maintenance schedule! Super cool work, and I continue to be convinced that we could be doing a lot more of this type of optimization in distributed systems than we are doing.</p></li></ul><p>Those were probably my top three talks at the conference, but there were just so many good talks that I feel compelled to put down a list of &#8220;honorable mentions&#8221;:</p><ul><li><p><a href="https://www.youtube.com/watch?v=GF6PqeV_l2s">From ESXi to Kubernetes: Modernizing 1,400 Edge Locations with Open Source</a>: this talk was a really nice &#8220;user story&#8221; about converting a giant grocery chain in Canada to Kubernetes.</p></li><li><p><a href="https://www.youtube.com/watch?v=e0_TSfDM2XY">The Case of the Misnamed Cities: CAST Analysis of a Google Maps Incident</a>: this talk was a cool post-incident analysis of a Google Maps issue from a few years ago, and showed compared a systems-theoretic approach to postmortems to a more traditional &#8220;root-cause analysis&#8221;.</p></li><li><p><a href="https://www.youtube.com/watch?v=jKLG4Jh7eRY">The Gashlycrumb Tinies of AI Networking You Must Know (or Languish!)</a>: this talk was <em>absolutely delightful</em> for the darkly gothic vibes, definitely worth the watch even if you don&#8217;t know or care about AI stuff.</p></li><li><p><a href="https://www.youtube.com/watch?v=F1JYRchNmdo">How Security Incidents Are Different &#8230; and How They&#8217;re Exactly the Same</a>: this was a deep dive into the lifecycle of a security incident by two former DataDog security engineers who would like me to stress that everything in this talk is STRICTLY HYPOTHETICAL and IS NOT FROM A REAL SECURITY INCIDENT. Anyways, tldr, security incidents are just like regular incidents except with more lawyers involved.</p></li></ul><h2>AI? In <em>my</em> SRECon?? It&#8217;s more likely than you think!</h2><p>Look, it wouldn&#8217;t be a blog post by me if I didn&#8217;t include <em>some</em> degree of complaining. And of course, given the current tech zeitgeist, that complaining is (of course) about Ayyyy Eyyyee. Every single talk (including mine, sadly) at least <em>mentioned</em> AI, and <em>many</em> of the talks were fully bought in on &#8220;AI all the things all the time&#8221;. The first plenary talk was about how we were are all going to survive as SREs in this new magical AI-ified world, and just&#8230; SIGH. I am&#8230; Just. So. Tired. This discourse is <em>exhausting</em> and it&#8217;s <em>everywhere</em>, even in the conferences that I love.</p><p>That said, the discourse about AI at this conference was&#8230; <em>somewhat</em>&#8230; more measured than your standard AI bro on LinkedIn. The first plenary talk (and a number of other talks) spent quite a bit of time talking about <a href="https://en.wikipedia.org/wiki/Jevons_paradox">Jevon&#8217;s Paradox</a>, which more-or-less says &#8220;the easier something is to produce, the <em>more</em> expertise you need in that field, not less.&#8221; The obvious application here is code generation: it&#8217;s now become trivially easy to generate hundreds of thousands of lines of sloppy code. So even <em>if</em><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a> gets better than your typical &#8220;expert software engineer&#8221;, and is able to significantly reduce the error rate in generated code compared to a human, that error rate is never going to go to zero, which means the absolute number of errors in code is going to <em>increase</em> because there will just be <em>so much code</em> out there.</p><p>Is this a hopeful view of reality? Eh. Not really, but, I mean, I think it&#8217;s a more <em>realistic</em> view of reality. I&#8217;m not worried about my job, as I&#8217;ve said many times before, I get paid a lot of money because I <em>am</em> an expert in the field. Do I hate that we&#8217;ve collectively turned our job description into &#8220;software plumbers&#8221;, aka, &#8220;manage all the shit that AI spews out&#8221;? Yes, yes I do. Am I going to continue forcing my way upstream against that trend as best as I can? Yes, yes I am. Am I confident that in a year or two ACRL is going to suddenly have a lot of business opportunities? Yes, yes I am.</p><p>But do I wish I could go to a conference and hear a little less about it? Sure do, Jan.</p><p>As always, thanks for reading!</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Originally LISA was the Large Installation System Administration conference, and then it expanded its scope slightly and (the story I heard was) that the acronym didn&#8217;t stand for anything any more, but I just looked on the Usenix website and even the most recent conference had the acronym expanded, so now I don&#8217;t know who to believe.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Two tracks, ballpark 1000 people&#8212;this honestly feels like the perfect size for a conference, imo.  Fewer than that and it feels like there&#8217;s not enough &#8220;content&#8221; (I&#8217;m using the term broadly) to justify the expense.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>I also love that every Usenix conference I&#8217;ve been to has an event coordinator just walk around the common area with chimes to announce the start of the next session. It&#8217;s very relaxing in a way.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>This is, of course, purely hypothetical and did not happen to yours truly at SRECon 2026 last month, and you will definitely never hear anything about this strange and extremely contrived circumstance again.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Aside from that one dudebro who tried really hard to insert himself into every conversation at the table and make it all about him, seriously, screw that guy.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>I actually wasn&#8217;t planning on going to any of these, because I didn&#8217;t really know what they were, but had someone I ran into in the hallway strongly encourage me to go to one, and I&#8217;m really glad I did!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>Nicole has also written a book called <a href="https://www.amazon.com/Frictionless-Remove-Barriers-Outpace-Competition/dp/1662966377">Frictionless</a>, discussing many of these same topics, which I have not read yet but am planning to read.... sometime when I have time again.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Drink!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p><a href="https://www.reddit.com/media?url=https%3A%2F%2Fpreview.redd.it%2Fthats-a-big-if-v0-kt157h7xkslb1.jpg%3Fauto%3Dwebp%26s%3Dafe464a63e0ab655d6a410636661f37a8e6d38a3">That&#8217;s a big if</a>.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Introducing SKEL: The SimKube Expression Language]]></title><description><![CDATA[We just released SimKube 2.5.0, which as I said on Mastodon, feels like a big milestone for mainly &#8220;I like round numbers&#8221; reasons; but I think this is actually a pretty major release, as it&#8217;s the first SimKube release to include support for a bespoke DSL that I&#8217;m calling SKEL, or the SimKube Expression Language.]]></description><link>https://blog.appliedcomputing.io/p/introducing-skel-the-simkube-expression</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/introducing-skel-the-simkube-expression</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 17 Apr 2026 23:30:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!KcHt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KcHt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KcHt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 424w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 848w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 1272w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KcHt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png" width="1125" height="1213" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1213,&quot;width&quot;:1125,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:259200,&quot;alt&quot;:&quot;An outline of a purple octopus with a poorly-drawn skull and femur bones for each of the tentacles.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/194567455?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="An outline of a purple octopus with a poorly-drawn skull and femur bones for each of the tentacles." title="An outline of a purple octopus with a poorly-drawn skull and femur bones for each of the tentacles." srcset="https://substackcdn.com/image/fetch/$s_!KcHt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 424w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 848w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 1272w, https://substackcdn.com/image/fetch/$s_!KcHt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0dd2ab27-46a5-4c65-83ed-c463cacc4188_1125x1213.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">This is what the inside of an octopus looks like, right???</figcaption></figure></div><p>We <a href="https://hachyderm.io/@drmorr/116409995083998309">just released SimKube 2.5.0</a>, which as I said on Mastodon, feels like a big milestone for mainly &#8220;I like round numbers&#8221; reasons; but I think this is actually a pretty major release, as it&#8217;s the first SimKube release to include support for a bespoke DSL that I&#8217;m calling SKEL, or the SimKube Expression Language. SKEL opens up a lot of <em>really interesting</em> possibilities for SimKube, so I wanted to spend some time in this post talking about it!</p><h2>Why do we need SKEL?</h2><p>As one of my friends likes to say, WPAYS?  This is an extremely dumb acronym that stands for &#8220;What Problem Are You Solving?&#8221;, which is nevertheless an important question to ask in many situations. In this case, the Problem I&#8217;m Solving&#8482;&#65039; is that SimKube trace files (which, as you may recall from previous hot garbage like <a href="https://blog.appliedcomputing.io/p/anatomy-of-a-trace">Anatomy of a Trace</a>, form the heart of the record-and-replay functionality in SimKube) are really hard to work with.</p><p>The main issue at play is that trace files are saved in the <a href="https://msgpack.io/">msgpack</a> format, which is a binary file format that is similar, but not completely identical to, JSON. The decision to use msgpack was made extremely early on in the life of SimKube; I was looking for a file format that would be relatively compact but also easy to work with<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, and the tagline on the msgpack website is &#8220;It&#8217;s like JSON, but fast and small,&#8221; and that sounded great to me. And, to be fair, it is both of those! The size difference between a SimKube trace file and the equivalent JSON text file is about 70%<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>! The main problem is that, unlike JSON and <a href="https://jqlang.org">jq</a>, there are no really ubiquitous tools for manipulating msgpack files. The best thing I&#8217;ve found is <a href="https://github.com/ludocode/msgpack-tools">msgpack-tools</a>, which lets you convert between msgpack and JSON; and, up until recently, that has been how I&#8217;ve worked with my trace files: convert the trace to JSON, make some changes, and convert back to msgpack.</p><p>This approach is fine, except that a) it&#8217;s not very user-friendly, and b) the msgpack schema and the JSON schema are very similar, but they are <em>not</em> identical. One of the big differences is that msgpack allows integer-valued map keys and JSON does not. And, in an unfortunate-but-somewhat-painful-to-fix design decision, SimKube trace files use integer map keys to track pod lifecycle events: that is, information like &#8220;this pod ran for this many seconds before terminating with this error code&#8221;. Because pod names are <em>not</em> a stable identifier, we use a hash of the pod spec to identify these lifecycle events, and that hash appears as a map key in the trace.</p><p>This is a problem for msgpack-tools: it can write out a JSON-like text file that includes these integer map keys, but it is completely unable to convert <em>back</em> to the msgpack format <em>from</em> this JSON-like text file because it first tries to parse the text file as JSON. This means that my &#8220;write to pseudo-JSON, make some changes, write back to msgpack&#8221; workflow is now entirely broken.</p><h2>If it&#8217;s so hard, how does anybody else ever work with msgpack???</h2><p>The main way that msgpack appears to be used in other applications is a serialization format for over-the-wire communication: meaning that you&#8217;ve got two programs on either end of a network connection that need to talk to each other. So, while msgpack doesn&#8217;t have a lot of CLI tools to support its use, it <em>does</em> have libraries to support serializing and deserializing msgpack for basically every programming language on the planet. So one option that I considered for solving the &#8220;trace modification problem&#8221; is &#8220;just make people write code&#8221;. We could provide a &#8220;SimKube trace modification library&#8221; that handles some standard sorts of operations, but otherwise allows users to write code to change the trace how they see fit using a msgpack library of their choice. This has the advantage of being <em>extremely</em> flexible, because users can do whatever they want to the trace, but also a bit of a usability nightmare: users now have to understand the internal trace file format, and if that format ever <em>changes</em>, it&#8217;s gonna break a ton of scripts<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>So instead, I opted for a narrower approach that still gives users flexibility, but doesn&#8217;t require them to understand the full internal workings of the trace file, and gives me the ability to change things about the trace file format in the future without breaking things for existing users: a domain-specific language. This approach, while requiring a bit more up-front work on my side, also means that I don&#8217;t have to provide &#8220;simkube libraries&#8221; for every programming language under the sun; I can continue to use Rust for everything, and users just have to learn an entirely new language all on their own! That seems like a fair tradeoff, right? Right???</p><h2>Designing SKEL</h2><p>I&#8217;ve not written a full-fledged DSL before, so this was also just an exciting opportunity for me to dip my toes in a new area of computer science. Before I started this whole project, I was dimly aware that there were things like &#8220;abstract syntax trees&#8221; and &#8220;lexers&#8221; and &#8220;parsers&#8221; and a whole bunch of other nonsense. But before diving into any of those details, I decided to just settle on a syntax for SKEL. I wanted the language to be relatively simple and intuitive, but I also knew that I had a wide variety of operations that it needed to be able to support.  I tried to make sure that the syntax could handle all of the transformations that I knew I needed, as well as being relatively extensible for the future.</p><p>To keep the language simple and tractable to implement, I set a few requirements for myself:</p><ol><li><p>Statements should be executed independently of each other; the only coupling between statements is how the impacts the underlying trace file<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a></p></li><li><p>There is no flow control (no loops, no conditionals, no gotos): every SKEL file is executed one line at a time in order</p></li><li><p>Since we&#8217;re focused on modifying trace files, each statement needs two parts: a <em>selector</em>, which specifies what bits of the trace file to operate on, and an <em>action</em>, which says what to do with those bits.</p></li></ol><p>I went through a few different options, including more of a SQL-like syntax, but I finally settled on a functional approach based on the third criteria above. Every statement in SKEL has the following syntax<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>:</p><pre><code><code>function(selector, action);</code></code></pre><p>A SKEL file then just looks like a sequence of these statements. The supported functions are pretty self-explanatory; right now we just support two, <code>remove</code> and <code>modify</code>; the <code>remove</code> function un-sets a field in the matching objects, and the <code>modify</code> function changes the value of a field in the matching objects. All of the magic is what happens <em>inside</em> these function calls.</p><h3>SKEL selectors</h3><p>If you recall from earlier posts on this blog, a SimKube trace is just a timeline of events: each event has a timestamp and a list of Kubernetes resources to apply or delete. So naturally, there are two types of selectors that SKEL supports: timestamp selectors and resource selectors. Timestamp selectors use the magic variable <code>@t</code>, and you can write any sort of relative or absolute query using this variable; <code>@t &gt; 5m</code> means &#8220;all the events after the 5-minute mark in the trace file&#8221;, for example.</p><p>Resource selectors are a bit more complicated, but just slightly; a SKEL resource selector uses jq-style &#8220;dot-notation&#8221; to dig into the internals of a Kubernetes resource: <code>metadata.labels."simkube.dev/whatever" == "foo"</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a> selects all of the resources in the trace that have a <code>simkube.dev/whatever=foo</code> label assigned to them. And of course, you can combine these two selectors: <code>@t &gt; 5m &amp;&amp; metadata.labels."simkube.dev/whatever" == "foo"</code> means &#8220;select all resources in the trace that were changed after 5 minutes, and have the appropriate label applied to them&#8221;.</p><p>The epiphany that I had while designing the syntax was that SKEL selectors define a <em>set</em> of events; the right way to think about writing SKEL commands is to decide what set of events you are trying to target, and then narrow in on that set using the <code>&amp;&amp;</code> operator. But, sometimes, in order to narrow in on the set of events, you need to be able to reference <em>other</em> fields in the resource. For example, you might want to target all resources that have a node selector matching the value of a particular label on that resource. This is where SKEL variables come into play!</p><pre><code><code>$x := metadata.labels."simkube.dev/whatever" | exists($x)
    &amp;&amp; spec.template.spec.nodeSelector.myNodeSelector == $x</code></code></pre><p>Here, we are defining a variable <code>$x</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a> that <em>points to</em> the value of the <code>simkube.dev/whatever</code> label; every statement in a SKEL selector must be a boolean, so in this case we just assert that this label is actually set on the object, and discard it otherwise. Then, the second statement narrows the scope to only target resources whose <code>myNodeSelector</code> matches the value of the <code>simkube.dev/whatever</code> label applied to that resource. This is very cool, in my opinion!</p><p>There&#8217;s one last feature that SKEL selectors support, which are wildcards. Some Kubernetes fields (like the <code>containers</code> field in the <code>PodSpec</code>) are arrays; we can target individual elements of those arrays using standard bracket notation (e.g., <code>spec.template.spec.containers[2]</code>), but in some cases we may want to target every element of an array; we can do that using the <code>*</code> wildcard. For example, <code>spec.template.spec.containers[*].image == "ubuntu:latest"</code> targets <em>every</em> container using the <code>ubuntu:latest</code> Docker image.</p><p>You can also mix-and-match variables and wildcards:</p><pre><code><code>$x := spec.template.spec.containers[*] | $x.image == "ubuntu:latest"</code></code></pre><p>defines <code>$x</code> as the set of full container specifications whose image field is <code>ubuntu:latest</code>. Super cool! I love how a (relatively) small set of selection operations has given rise to a very rich ecosystem of capabilities.</p><h3>SKEL actions</h3><p>This section is comparatively shorter than the selectors section; that&#8217;s because, once you&#8217;ve defined a set of events and resources to take action on, actually <em>performing</em> those actions is pretty straightforward. For example,</p><pre><code><code>remove(metadata.labels."simkube.dev/whatever" == "foo", spec.template.spec.nodeSelector);</code></code></pre><p>just removes the <code>nodeSelector</code> field from every resource with the <code>simkube.dev/whatever=foo</code> label applied to it. Similarly, the <code>modify</code> command would allow you to <em>change</em> the value of the nodeSelector instead of removing it. Of course, variables and wildcards also work inside the &#8220;action&#8221; side of the command, which lets you build up extremely targeted operations to your trace file as well.</p><h2>Implementing SKEL</h2><p>I&#8217;ll try to keep this section brief, but for all you programming language theory nerds out there<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a> I&#8217;ll talk a bit about the implementation details of SKEL. The core library that I&#8217;m using is called <a href="https://pest.rs/">pest</a>, which continues in the grand tradition of &#8220;cool Rust projects with really questionable names&#8221;<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>. Pest implements a <a href="https://en.wikipedia.org/wiki/Parsing_expression_grammar">Parsing Expression Grammar</a> library in Rust.  To use it, you define the DSL grammar in a <code>.pest</code> file, and Pest will automagically convert the grammar into a set of Rust enums representing all of the grammar&#8217;s rules. So, for example, one of the PEG rules in my pest file looks like the following<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>:</p><pre><code><code>conditional_test = { resource_path ~ comparator ~ (val | resource_path) }</code></code></pre><p>This defines a grammar rule called <code>conditional_test</code> (accessed in the Rust code as <code>Rule::conditional_test</code>), and a <code>conditional_test</code> consists of a <code>resource_path</code> followed by a <code>comparator</code>, and either an atomic value (like a string or an integer) or another <code>resource_path</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>.</p><p>One of the interesting things about Pest is that the library itself asserts which rules are valid in which positions, so it is idiomatic in Pest code to sprinkle <code>.unwrap()</code>s and <code>unreachable!()</code>s everywhere based on where you are in the parsed expression. This takes a bit of getting used to.</p><p>Now if you know anything about designing programming languages, you&#8217;re probably expecting that the next words out of my mouth are going to be &#8220;abstract syntax tree&#8221;. But, you&#8217;d be wrong. An abstract syntax tree is a data structure that, more-or-less<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>, lets you unambiguously specify order of operations: in other words, all those pesky rules you learned in grade school about whether you add things before you multiply things. However, for SKEL, this turns out to be entirely unnecessary: there is one unambiguous way to parse a SKEL file, and that&#8217;s from left-to-right. I no longer have an abstract syntax tree, I have more of an&#8230; abstract syntax path. This turns out to be very handy. It means that all of the logic for SKEL is contained in about 2000 lines of code, much of which is pretty straightforward &#8220;convert this Rule into this data structure&#8221;.</p><p>Anyways, once I&#8217;ve loaded everything into my ASP, I can just traverse through all the statements in the file and apply them to the input trace one-by-one. The mechanics of this are pretty straightforward, the most complicated bits were just about keeping track of the variable state so that I can correctly know which variables are defined at which point in time, and to what values they point.</p><p>There was one open question about the order in which statements should be applied: should I apply each statement to the <em>entire</em> trace file before moving on to the next one? Or should I loop through the trace file once, and apply every statement in order to each event? In the interest of &#8220;keeping statements and effects local&#8221;, I elected for the former; in other words, once an event in the trace has been processed by all SKEL statements, it will never be touched again. It &#8220;feels like&#8221; this will be easier to reason about in the long run, but I don&#8217;t actually know for sure.</p><h2>Deleting code feels good man</h2><p>I&#8217;ll just wrap this post up by saying that SKEL is actually the second attempt I&#8217;ve made to solve this problem; about a year ago, I wrote a bunch of code to &#8220;validate&#8221; a trace file, and confirm that it won&#8217;t cause any common problems in a simulation (example of a common problem: a Pod needs a Secret injected as an environment variable, but the Secret isn&#8217;t defined in the simulation environment, which probably you do not actually want, and the value of the Secret doesn&#8217;t actually matter anyways because there&#8217;s no code running in the Pod to use the Secret). One of the features in this validator allowed users to auto-apply fixes to the generated trace; unfortunately, for every class of problems, there are many different types of fixes that you could apply. In the Secret example above, for instance, you could <em>either</em> remove the secret from the pod specification, <em>or</em> you could create the secret with a placeholder value at the beginning of the simulation. And who knows? Maybe <em>neither</em> of these solutions is what you actually want to do.</p><p>The logic to keep track of these potential fixes, and which once were applicable at which times to what events, was some of the ugliest, most complicated code in the entire codebase. I had visions of expanding the feature, and eventually making it so that you could even have user-editable &#8220;fixes&#8221;, but the code was so gross already that I just kindof abandoned it. It worked! But it wasn&#8217;t pretty and I didn&#8217;t want to add anything else to it.</p><p>However, with the advent of SKEL, I was finally able to just rip all that out and simplify a lot of things: now, instead of auto-fixing issues, the SimKube validation code just prints out a sample SKEL file that will address the issues that were detected in the trace; users can look at that and decide whether those are the correct fixes, or if they&#8217;d like something different. Then they can modify it how they&#8217;d like before applying the transformations and running a simulation. Even with all the complexity of the DSL and everything, this is a much better approach! And I&#8217;m very happy about it.</p><p>Everything we&#8217;ve talked about is now available in <a href="https://github.com/acrlabs/simkube/tree/v2.5.0">SimKube 2.5.0</a> and there&#8217;s a bunch more details in <a href="https://simkube.dev/simkube/docs/skel/intro/">the documentation</a> as well, but this has been a really fun new addition to SimKube, and I think it&#8217;s going to open up a lot of possibilities in the future!</p><p>I hope you enjoyed reading this as much as I enjoyed building it, and as always&#8212;thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I was also looking for something that wasn&#8217;t <a href="https://protobuf.dev/">protobuf</a>, cuz I hate dealing with that format.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>You can get it down even smaller, to about 99% compression if you run the msgpack trace file through gzip compression; if you were to just gzip the equivalent JSON file, you &#8220;only&#8221; get about 94% compression.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>You know, because by that point I&#8217;ll be wildly successful and will have <a href="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNW14ZmpnZmh3OHc1ZzltdGk5MGMxeWtvdnhhYXlqNGhlOGpka20xNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/kSlJtVrqxDYKk/giphy.gif">dozens</a> of people using SimKube!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>This decision &#8220;might&#8221; be a bad one; the implication is that the order of your statements matters. If you modify all the pods to remove the <code>simkube.dev/whatever</code> label, and then later select pods based on the <code>simkube.dev/whatever</code> label, it will select nothing. On the other hand, if you reverse the order of the statements, the selection set will be non-empty.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Yes, all statements end with a semi-colon. Don&#8217;t at me, bro.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>Since most Kubernetes labels, annotations, and a few other fields use domain names (e.g. <code>simkube.dev</code>) for namespacing, there has to be a way for SKEL to differentiate between the usage of periods, hence the double-quotes around the label key.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>Perl called, they want their dollar signs back.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>I know some of you follow this blog, don&#8217;t even try to hide it.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Other projects in this category include: clap, anyhow, rustls (seriously, how do you even pronounce that???), and, of course, SimKube.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>Brand new sentence.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p>If you&#8217;re curious, you can see the <a href="https://github.com/acrlabs/simkube/blob/main/sk-cli/src/skel/skel.pest">full grammar file</a> on GitHub.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p>I am not a programming languages person, but I am 100% convinced that all of you who are PL people are already getting out your wallets to become paid subscribers so you can tell me how wrong I am in the comments.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Boy AMI glad to see you]]></title><description><![CDATA[We&#8217;re pleased to announce that two AMIs (Amazon Machine Images) have joined the SimKube family!]]></description><link>https://blog.appliedcomputing.io/p/boy-ami-glad-to-see-you</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/boy-ami-glad-to-see-you</guid><dc:creator><![CDATA[Ian O’Gorman]]></dc:creator><pubDate>Fri, 03 Apr 2026 23:05:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QTEp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We&#8217;re pleased to announce that two AMIs (<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html">Amazon Machine Images</a>) have joined the <a href="https://simkube.dev/">SimKube</a> family! Yes, twins: <a href="https://aws.amazon.com/marketplace/pp/prodview-m7imofdta3tla?sr=0-1&amp;ref_=beagle&amp;applicationId=AWSMPContessa">simkube-x86-64</a> and <a href="https://aws.amazon.com/marketplace/pp/prodview-jea6uc3po665a?sr=0-2&amp;ref_=beagle&amp;applicationId=AWSMPContessa">simkube-github-runner-x86-64</a> are now available in the AWS Marketplace. Each came in at a healthy 17 GiB snapshot weight<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. They arrived about two months apart due to the famously transparent AWS Marketplace approval process.</p><p>I&#8217;ll explain what each of these AMIs are and how we build them in due course, but first off, let&#8217;s address an important question:</p><h2>Two AMIs, in THIS economy?</h2><p>We know it&#8217;s crazy, who even has the action minutes to raise AMIs these days; we sure don&#8217;t. But we had a problem, or maybe an opportunity. SimKube just keeps getting better and better but configuring it can be, frankly, difficult. Building high-fidelity simulation environments requires installing and configuring a long list of tools: <a href="https://kind.sigs.k8s.io/">kind</a>, <a href="https://kwok.sigs.k8s.io/">KWOK</a>, <a href="https://kubernetes.io/docs/reference/kubectl/">kubectl</a>, <a href="https://docs.docker.com/">Docker</a>, <a href="https://prometheus.io/docs/introduction/overview/">prometheus</a>, and SimKube, to name a few. So spinning up a ready-to-go SimKube environment takes some doing.</p><p>Internally, we have a configuration management repository called <a href="https://open.substack.com/pub/appliedcomputing/p/what-to-expect-when-youre-expecting?r=6zc8zh&amp;utm_campaign=post&amp;utm_medium=web">isengard</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. It is ~48k lines of pure <a href="https://docs.ansible.com/">Ansible</a> bliss. We use it to automate the deployment of repeatable simulation environments. It occurred to us that users of SimKube probably don&#8217;t want step one of using it to be &#8220;here&#8217;s ~48k lines of Ansible, good luck!&#8221;. It turns out there is a better way: a custom SimKube AMI.</p><h2>Why not a Docker image like a normal person?</h2><p>That&#8217;s a fair question. We did evaluate using a Docker image because one of our primary goals is a fast, one-click startup.</p><p>The challenge is that SimKube relies on <code>kind</code>, which spins up Kubernetes nodes as Docker containers. Initializing and configuring the kind cluster requires access to a live Docker daemon. During <code>docker build</code>, there is no Docker daemon available inside the build environment, which means we can&#8217;t just &#8220;run all the setup steps in our Dockerfile&#8221; and ship the result.</p><p>We also looked at snapshotting a running Docker environment, but that&#8217;s complicated for a different set of reasons. So after a long side quest that included Vagrant and QEMU, we realized what we actually need isn&#8217;t a container image but a prebuilt machine image that preserves the state of our configured simulation cluster. Since we primarily work in AWS, an AMI fits naturally.</p><h2>Baking the AMIs</h2><p>Fortunately, baking AMIs is a fairly straightforward task that our ancestors have been doing for thousands of years. We can reuse a lot of what we have already built in our configuration management system (which I will remind you is lots and lots of Ansible). We use <a href="https://developer.hashicorp.com/packer/docs">Packer</a> to bake our AMIs, so the first step is selecting a base image which our custom AMI will be built on top of. We chose Ubuntu 24.04 LTS for its stability, compatibility with our tooling, and long term security patching.</p><p>Using Packer we can initiate an automated build via a GitHub Action. For configuration, Packer includes a range of provisioners&#8212;<a href="https://developer.hashicorp.com/packer/integrations/hashicorp/ansible/latest/components/provisioner/ansible">including one for Ansible</a>&#8212;so we are able to leverage our existing configuration library in <code>isengard</code>. The GitHub Action itself is fairly simple: it clones the repo and runs packer. This helps keep our Packer configuration sparse and maintainable. We only need to configure a handful of things: the Ansible playbook to run, the region of our builder, our base AMI, regions to copy the finished AMI to, and any cleanup activities or supplemental provisioners.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BUDz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BUDz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 424w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 848w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 1272w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BUDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png" width="1096" height="1274" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1274,&quot;width&quot;:1096,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:125032,&quot;alt&quot;:&quot;A screenshot of a an AMI pipeline showing the relationships between GitHub Actions, Packer and Ansible&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/188559350?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A screenshot of a an AMI pipeline showing the relationships between GitHub Actions, Packer and Ansible" title="A screenshot of a an AMI pipeline showing the relationships between GitHub Actions, Packer and Ansible" srcset="https://substackcdn.com/image/fetch/$s_!BUDz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 424w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 848w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 1272w, https://substackcdn.com/image/fetch/$s_!BUDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e77b3a5-92ea-4c91-8cc6-c8822b3c8368_1096x1274.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">An AMI pipeline we can live with.</figcaption></figure></div><p></p><p>Our AMI pipeline is triggered by a weekly cron trigger, or by a manual build via a dispatch trigger<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. After some bake time, we end up with a custom AMI image backed by the snapshot created during the build. That AMI is then listed on the Marketplace.</p><h2>AMI patching</h2><p>Shipping a public AMI means we own patching it. At a minimum, Ubuntu is going to ship security patches (we really want those) and there will be patches for other software in our stack. Instead of patching in place, we treat each AMI build as an immutable artifact tied to the configuration management repository (isengard) git hash used to produce it. Every build is traceable and reproducible.</p><p>Every time our pipeline runs, it starts fresh with a clean Ubuntu image and pulls down the latest patches so each AMI is fully up to date at build time. The result is a simple, deterministic build process that is easy to maintain. AMIs are short-lived, they won&#8217;t drift over time, and there is no ambiguity about which code produced which image.</p><p>The tradeoff is that older AMIs are never patched. If you launch an older version, you get exactly what existed at build time. For our use case, this ends up being a feature since we value reproducibility. This comes in handy for debugging thorny issues in our AMIs, especially those that manage to bypass our validation tests. We can fire up an AWS EC2 instance and watch one of our services get clobbered in real time by some bad code that I definitely didn&#8217;t write.</p><p>For the most part our AMI pipeline quietly churns out new images. To keep our account from piling up with tons of old AMIs<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>, we use a <a href="https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/build/post-processor">Packer post-processing block</a> to deprecate old AMIs and clean up their snapshots automatically.</p><h2>You said TWO AMIs</h2><p>I did say that! We have two versions of our AMI. The first is the <a href="https://aws.amazon.com/marketplace/pp/prodview-m7imofdta3tla?sr=0-1&amp;ref_=beagle&amp;applicationId=AWSMPContessa">free SimKube AMI</a> with everything needed to run SimKube including a running kind cluster and management tools. This is our free-to-use simulation environment. All the user needs to do is launch it in AWS EC2 and get right to running simulations&#8212;though you will need a trace from the cluster you are simulating.</p><p>The second AMI is our <a href="https://aws.amazon.com/marketplace/pp/prodview-jea6uc3po665a?sr=0-2&amp;ref_=beagle&amp;applicationId=AWSMPContessa">SimKube GitHub Action Runner</a> it includes everything in the SimKube AMI but also has some extra configuration applied. We use an iterative build process, so this version is literally built on top of the base SimKube AMI<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>.</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QTEp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QTEp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 424w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 848w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 1272w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QTEp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png" width="1127" height="646" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:646,&quot;width&quot;:1127,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89704,&quot;alt&quot;:&quot;A screenshot of a an AMI lineage diagram showing the inheritance of AMIs from Ubuntu 24.04 LTS,   down to simkube-x86-64, and finally to simkube-github-runner-x86-64&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/188559350?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A screenshot of a an AMI lineage diagram showing the inheritance of AMIs from Ubuntu 24.04 LTS,   down to simkube-x86-64, and finally to simkube-github-runner-x86-64" title="A screenshot of a an AMI lineage diagram showing the inheritance of AMIs from Ubuntu 24.04 LTS,   down to simkube-x86-64, and finally to simkube-github-runner-x86-64" srcset="https://substackcdn.com/image/fetch/$s_!QTEp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 424w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 848w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 1272w, https://substackcdn.com/image/fetch/$s_!QTEp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0757ed00-3b2b-4de0-9e0d-07892ebd7d69_1127x646.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Built on the shoulders of giants.</figcaption></figure></div><p></p><p>This cuts down on build time and allows us to patch the GitHub Action Runner software independently of the base AMI if we wish. The additional configuration in this version is the GitHub runner software and a systemd wrapper that manages it. We use this version to run SimKube in CI pipelines (via <a href="https://docs.github.com/en/actions/get-started/understand-github-actions">GitHub Actions</a>). Effectively, this AMI is primed to register itself with a GitHub repo as a custom action runner when it receives the information contained in our User Data script.</p><h2>A world of opportunities</h2><p>Our SimKube AMI is a step forward in making SimKube approachable and easy to use. Instead of spending a few hours setting up a simulation environment, you can grab the SimKube AMI off the AWS Marketplace and have SimKube up and running in a couple of minutes. You will need to <a href="https://simkube.dev/simkube/docs/intro/running/#step-1-collect-a-trace">grab a trace</a> from your production cluster, but the environment for running those simulations is available at the click of a button or at the end of a AWS CLI command.</p><p>We want to continue to extend Kubernetes simulation into CI pipelines using our GitHub runner AMI. The vision is an engineer, maybe you, checks in some change to your cluster. Then, SimKube CI simulates it based on your production cluster and sends you back metrics you can use to evaluate your change before it hits production.</p><p>Today, ACRL is already running small simulations in CI in the <a href="https://github.com/acrlabs/simkube/blob/main/.github/workflows/simkube_e2e.yml">SimKube repo</a>. We have developed custom GitHub Actions, available in our <a href="https://github.com/acrlabs/simkube-ci-action">simkube-ci-action</a> repo, to make launching runners backed by SimKube AMIs as easy as adding a few lines in your GitHub Actions workflow.</p><p>So maybe you find SimKube interesting but setting it up has been too much of a hassle. Or perhaps you are already running SimKube locally but want to run a dozen simultaneous simulations in AWS. The AMIs are there for you, and the SimKube AMI is free-to-use&#8211;though you still have to pay AWS for the compute (sorry).</p><p>If you want to learn more, we&#8217;ve added a new <a href="https://simkube.dev/simkube/docs/infra/overview/">SimKube in the Cloud</a> section to the documentation that walks through how they work and how to get started.</p><p>So get out there and simulate some trouble&#8230; before it makes it to prod!</p><p>Cheers,</p><p>Ian</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Applied Computing Research Labs is a reader-supported publication. All our footnotes are ethically sourced.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Is snapshot weight part of the APGAR score?</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Pronounced: nerds</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Builds are expensive from an action minutes perspective</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Ask me how I know that EBS volume storage costs extra</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Andddd now the twins metaphor has completely broken down</p></div></div>]]></content:encoded></item><item><title><![CDATA[Stop Reading Changelogs: Safer Kubernetes upgrades with simulation]]></title><description><![CDATA[SRECon 2026 Talk Transcript]]></description><link>https://blog.appliedcomputing.io/p/stop-reading-changelogs-safer-kubernetes</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/stop-reading-changelogs-safer-kubernetes</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Fri, 27 Mar 2026 20:01:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YQ3N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YQ3N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YQ3N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 424w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 848w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YQ3N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/df09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:179927,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/192347873?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YQ3N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 424w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 848w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!YQ3N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf09fd51-ca0b-497c-9370-9de2bc70b309_1960x1104.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Last week I had the privilege of giving a talk at <a href="https://www.usenix.org/conference/srecon26americas">SRECon Americas</a> in Seattle; the <a href="https://www.youtube.com/watch?v=yk_POo6dwZA">recording of the talk</a> is available on the Usenix YouTube channel, but I thought for this post I&#8217;d do something a bit different and write an (approximate) transcript of the talk for those of you who don&#8217;t want to wait and/or prefer reading things to watching videos. The talk is, of course, about SimKube: I hope you enjoy! I&#8217;ll update this with a link to the talk once it&#8217;s posted.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.appliedcomputing.io/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Applied Computing Research Labs is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p>Thanks all for coming; the title of my talk today is &#8220;Stop Reading Changelogs: Safer Kubernetes Upgrades with Simulation&#8221;.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0N2S!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0N2S!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 424w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 848w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0N2S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:432618,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/192347873?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0N2S!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 424w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 848w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!0N2S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20cb6f70-f43e-43dc-8d50-9587c9745e23_1960x1104.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>On Pi Day three years ago, March 14, 2023, Reddit suffered a 100-pi-minute long outage, a 314-minute outage. In the extremely detailed and <a href="https://www.reddit.com/r/RedditEng/comments/11xx5o0/you_broke_reddit_the_piday_outage/">well-written postmortem</a>, it was revealed that one of the primary causes of this outage was a Kubernetes version upgrade from Kubernetes 1.23 to 1.24. Specifically, in this upgrade, Kubernetes changed one of the labels that was applied to its control plane nodes, and Calico, the service mesh component used by Reddit at the time, was using a label selector that pointed to the <em>old</em> label for the node instead of the new label. This caused Reddit to go hard down for around 5 hours.</p>
      <p>
          <a href="https://blog.appliedcomputing.io/p/stop-reading-changelogs-safer-kubernetes">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Everything you need to run a single Kubernetes pod]]></title><description><![CDATA[OK, as promised last week, this post is a follow-on to my previous write-up about running a single-node Kubernetes cluster at ACRL.]]></description><link>https://blog.appliedcomputing.io/p/everything-you-need-to-run-a-single</link><guid isPermaLink="false">https://blog.appliedcomputing.io/p/everything-you-need-to-run-a-single</guid><dc:creator><![CDATA[drmorr]]></dc:creator><pubDate>Sat, 14 Mar 2026 20:00:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!heC4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!heC4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!heC4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 424w, https://substackcdn.com/image/fetch/$s_!heC4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 848w, https://substackcdn.com/image/fetch/$s_!heC4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 1272w, https://substackcdn.com/image/fetch/$s_!heC4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!heC4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png" width="1456" height="1035" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1035,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:760338,&quot;alt&quot;:&quot;Architecture diagram showing Moria, Isengard, and Mirkwood all interacting with AWS to run a Kubernetes cluster&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.appliedcomputing.io/i/190960222?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Architecture diagram showing Moria, Isengard, and Mirkwood all interacting with AWS to run a Kubernetes cluster" title="Architecture diagram showing Moria, Isengard, and Mirkwood all interacting with AWS to run a Kubernetes cluster" srcset="https://substackcdn.com/image/fetch/$s_!heC4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 424w, https://substackcdn.com/image/fetch/$s_!heC4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 848w, https://substackcdn.com/image/fetch/$s_!heC4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 1272w, https://substackcdn.com/image/fetch/$s_!heC4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8c4e84e-afad-4257-9ba3-bb312977fba0_2568x1825.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I built all this stuff so that you don&#8217;t have to.</figcaption></figure></div><p>OK, as promised last week, this post is a follow-on to my <a href="https://blog.appliedcomputing.io/p/what-to-expect-when-youre-expecting">previous write-up</a> about running a single-node Kubernetes cluster at ACRL. After calling that post &#8220;hot garbage&#8221;, the redditor who commented on the post went on to say that he was hoping for more details and less high-level pish-tosh<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. So, random redditor, this 4000-word manuscript is for you! I hope you brought your coffee.</p><h2>Project Goals: Quick Recap</h2><p>Before we dive into all the arrows in the architecture diagram, I just wanted to quickly review what the goals of this project are:</p><ol><li><p>Run a Kubernetes cluster in the cloud,</p></li><li><p>as cheaply as possible,</p></li><li><p>and schedule a workload on it. Lastly,</p></li><li><p>it should be one command to completely destroy and recreate our entire stack if we need to.</p></li></ol><p>To recap what I discussed in the previous post, we&#8217;re running <a href="https://k3s.io/">k3s</a> on a single AWS EC2 spot instance. We&#8217;re not using <a href="https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html">EKS</a>, a) because it&#8217;s expensive, and b) because we&#8217;re supposed to Kubernetes experts over here and not just some random hack jobs, so we should probably have some expertise running Kubernetes. We&#8217;re running on a spot instance a) because it&#8217;s cheap, and b) because I wanted to know if it could be done.  If you&#8217;re following along at home and you&#8217;re not running on a spot instance, you can <em>probably</em> do away with a lot of this complexity; but on the flip side, running on spot serves as a forcing function to build the automation and tooling to easily recreate your entire state from scratch, which is in my experience an extremely underrated superpower.</p><p>In my previous post, we got to the point of &#8220;having a running k3s node&#8221;, but we didn&#8217;t actually get to the point of &#8220;running a workload on it&#8221;, partly because I ran out of time, and partly because I thought that once I had the node up and running, running the pods would be easy. It&#8217;s just YAML, right<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>???</p><p>Anyways, in the last post we also didn&#8217;t define what workload we&#8217;re going to be running on our Kubernetes cluster.  I have quite a few things I eventually want to run, but my first goal was just to run a simple XMPP server. I&#8217;ve wanted a good internal &#8220;chat&#8221; solution for a while, I <em>really</em> don&#8217;t want to use Slack or Discord, and it turns out that XMPP has been living a healthy thriving life for the past 15 years, even if most people don&#8217;t realize it. So after a bit of research, I decided that I was going to get <a href="https://prosody.im/">Prosody</a> running inside my Kubernetes cluster. It&#8217;s small and lightweight enough that &#8220;configuring Prosody&#8221; wouldn&#8217;t take too long, but it would also exercise a bunch of features that I&#8217;m going to want later: specifically, ingress, certificates, and persistent data.</p><p>I <em>also</em> also didn&#8217;t explicitly discuss the fourth goal in my last post, but this is really important to me. I&#8217;m a very big proponent of the &#8220;GitOps&#8221; pattern<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. If you can check in your desired infrastructure state into version control, and it&#8217;s easy to recreate and/or get back to a &#8220;known good configuration&#8221;, that makes everything so much easier down the line. So for me at least, it&#8217;s worth it to spend some more time up front doing things &#8220;right&#8221; to (hopefully) make my life a little easier in the future<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p><p>So with that said, let&#8217;s go into the nitty-gritty details.</p><h2>Revisiting Middle-earth</h2><p>In my last blog post I explained that I have two repos named after famous locations from Lord of the Rings: Moria handles my &#8220;infrastructure as code&#8221; and Isengard is my server configuration management repo. Since that post, I&#8217;ve added a third git repo, Mirkwood, which contains all of my Kubernetes manifests<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. You can see in the above diagram how each of these repos interact with components in AWS, shown by the colored solid arrows (green is moria, blue is isengard, orange is mirkwood, and the dashed lines indicate physical hardware connections or network connections); feel free to refer back to that diagram as we continue.</p><h3>Moria</h3><p>My last post did a pretty good job of covering the basic AWS infrastructure setup, so I&#8217;ll just quickly recap it here. In Moria, we use <a href="https://www.pulumi.com/">Pulumi</a> to create the following AWS resources:</p><ol><li><p>A single bare EC2 node as an SSH proxy/bastion host/NAT gateway into my VPC.</p></li><li><p>A single-node AutoScaling Group (ASG) which contains the k3s server node, registered as a spot instance.</p></li><li><p>A persistent EBS volume which is auto-attached to the k3s server node on first boot (via a systemd service), which serves as the data volume for k3s.</p></li><li><p>An internal zone in Route53 which I can use to store a DNS A record pointing to the k3s server.</p></li><li><p>An AWS Lambda function, run as an ASG Lifecycle Hook which updates the A record whenever the k3s server is terminated and recreated.</p></li></ol><p>Since the first post, I&#8217;ve had to add a number of other AWS resources as well into Moria to support various tooling further down the stack. XMPP requires TLS these days, which means I need a certificate; I considered whether to use <a href="https://blog.appliedcomputing.io/p/acrl-is-a-ca-now">the CA</a> that I set up earlier in the year for this purpose, but those certificates are for a <em>very</em> different purpose than what I want now. I also considered whether to create a second CA for &#8220;internal services&#8221; but that also didn&#8217;t feel like a good approach. Fortunately, <a href="https://letsencrypt.org/">Let&#8217;s Encrypt</a> is a thing; I&#8217;ve been using it for a long time for my personal website, and it has good integrations with Kubernetes, so this felt like the right approach.</p><p>Certificates need to be signed for a particular domain, however, and Let&#8217;s Encrypt needs to know that you own the domain that it&#8217;s signing certificates for. But my Kubernetes cluster is inside a private VPC that Let&#8217;s Encrypt knows nothing about! So this means I need to use Moria to add</p><ol start="6"><li><p>Two more hosted zones<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a> into my infrastructure; a public zone that Let&#8217;s Encrypt can use to verify ownership, and a private zone that will host the A records for my XMPP server. Note that we don&#8217;t actually configure those A records with Moria, however, because the IP address of Prosody can change.</p></li></ol><p>I also didn&#8217;t talk about this at all in the last post<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-8" href="#footnote-8" target="_self">8</a>, but Moria also manages</p><ol start="7"><li><p>All of the AWS IAM (Identity and Access Management) permissions for this configuration.</p></li><li><p>An encryption key in AWS KMS (Key Management Service) to help with secure secret storage.</p></li></ol><p>IAM permissions are some of the most arcane and convoluted things I have ever had to work with, but essentially every component above requires a corresponding IAM policy that enables it to perform its task. The proper, most secure way of managing these permissions involves things like short-lived temporary credentials and OIDC and JWT and a whole bunch of other ugly acronyms, but frankly that stuff gives me heartburn every time I look at it, so for right now I just have a bunch of &#8220;service account&#8221; IAM users with static credentials configured in Moria. This is marginally less-secure, but I make it easy in Moria to rotate the credentials, and try to keep &#8220;what they can access&#8221; scoped as narrowly as possible. Someday in the future when I can afford to hire an infrastructure security engineer, I&#8217;ll make their first task to rip out all the service account users and replace it with the OIDC thing<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-9" href="#footnote-9" target="_self">9</a>.</p><p>I&#8217;ll lastly mention one note about the single-node spot instance: previously I had configured my ASG to launch a <code>t3a.medium</code>, which uses an AMD processor, has 2 CPUs, and 4GB of RAM. My expectation was that, since this is a (relatively) small instance type, it would have pretty high availability, since AWS can probably bin-pack that more-or-less anywhere. That expectation was extremely incorrect; the k3s server node was getting disrupted between 5 and 10 times a day. Since then, I&#8217;ve expanded to allowing any of <code>t3a.medium</code>, <code>t3.medium</code>, <code>t3a.large</code>, or <code>t3.large</code> spot instance types<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-10" href="#footnote-10" target="_self">10</a>, and using the &#8220;price capacity optimized&#8221; allocation strategy: this essentially tells AWS &#8220;pick an instance type that is relatively cheap, but also is relatively unlikely to get interrupted&#8221;. Since I made that change, I&#8217;ve been running a <code>t3.medium</code> spot instance for the last 3 days with no interruptions!</p><h3>Isengard</h3><p>In the last post, I gave an <em>extremely</em> high-level overview of my Isengard (<a href="https://ansible.com/">Ansible</a>) configuration management setup. Just as Pulumi or Terraform let you define AWS resources as code, Ansible lets you define <em>software resources</em> as code. In Ansible, you write all of your software configuration using a combination of YAML files and <a href="https://jinja.palletsprojects.com/en/stable/templates/">Jinja</a> templates. These configuration steps are bundles into &#8220;roles&#8221; which are then rolled up into &#8220;playbooks&#8221;. The idea is that a server can have multiple roles, each of which has its own software and configuration installed, and the playbook is what tells Ansible which roles should be applied to which hosts.</p><p>In Isengard, we have three playbooks:</p><ol><li><p>The SimKube playbook: this is used to quickly and repeatably create SimKube environments. It&#8217;s also what&#8217;s powering our SimKube AMI and Github Action Runners, which I&#8217;ve been teasing in several posts, but will be getting its own blog post very soon now. So I won&#8217;t say anything further about this.</p></li><li><p>The bastion playbook: this playbook manages the &#8220;entry point&#8221; into our private VPC.</p></li><li><p>The k3s server node playbook, (also used to generate the AMI that packer builds for the k3s root volume).</p></li></ol><p>In the previous post, I mentioned that I was using SSH tunneling through this bastion host, but that has since changed. I configured <a href="https://tailscale.com/">Tailscale</a>, and I have no other words for it beyond &#8220;freaking magical.&#8221; I understand (somewhat, at a high level) how Tailscale works, but the experience has been so unbelievably good that I wish I&#8217;d done this years ago. Everything Just Works; I now have my bastion host configured to forward both DNS lookups into my private VPC, as well as to expose the internal routing tables from my VPC to any other client in the tailnet. It really is magical, there&#8217;s just no other way to describe it.</p><p>I am also running the <a href="https://github.com/AndrewGuenther/fck-nat">fck_nat</a> NAT gateway on my bastion node, to allow everything <em>inside</em> the VPC to talk to The Internet :tm:. AWS <em>does</em> actually provide a native/built-in solution for this, but it&#8217;s frankly hard to describe it as anything other than price gouging. A single <a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html">NAT gateway</a> instance costs $30/month <em>just to exist</em>, and then they charge you on top of that for all of the traffic that goes through the NAT! For comparison, I&#8217;m running <code>fck_nat</code> on a <code>t4g.nano</code>, which works just fine and costs me $3/month.</p><p>On the k3s server node, I am obviously using Ansible to install k3s, kubectl, and other supporting tools. I also configure the k3s systemd service here. This is what my k3s config looks like:</p><pre><code><code>---
disable-helm-controller: true
disable-network-policy: true
tls-san: "k3s-server-0.uswest2.acrl.dev"
secrets-encryption: true</code></code></pre><p>I disable helm because, ew gross. I disable the network policy controller because I don&#8217;t care about network policies, and that controller expects a stable IP address which we obviously don&#8217;t have. The <code>tls-san</code> block gives a stable domain name to issue the Kubernetes certificates for<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-11" href="#footnote-11" target="_self">11</a>, and the secrets encryption line enables AES at-rest encryption for Kubernetes Secret objects.</p><p>For a brief period in time I also specified a stable Kubernetes node name in this config, but that ended up causing more problems than it solved:  whenever the node was interrupted/restarted, it would remember all of the state of the node from before it was interrupted, which led to some extremely weird &#8220;stale state&#8221; issues.  More on this later.</p><p>For what are probably &#8220;premature optimization&#8221; reasons, this config file is <em>not</em> baked in the k3s AMI, but instead is generated as an <code>ExecStartPre</code> script in systemd; the script looks up the <code>tls-san</code> name from a tag on the EC2 instance, which allows me to dynamically detect the hostname and potentially run more of these clusters in the future.</p><p>There is also an <code>ExecStartPost</code> k3s systemd hook which is configured by Isengard that does the following:</p><ol><li><p>Applies the EBS CSI Driver &#8220;not-ready&#8221; taint to the node (if this doesn&#8217;t mean anything to you yet, don&#8217;t worry, just keep reading).</p></li><li><p>Cleans up any Node resources that are left over from when the spot instance was restarted.</p></li></ol><p>The latter point is necessary because Kubernetes doesn&#8217;t actually <em>ever</em> delete nodes that are stored in its database, it just lists them as <code>NotReady</code> in perpetuity. This isn&#8217;t <em>really</em> a problem, except that it looks ugly and takes up space. If we were not using k3s, we would configure the <a href="https://kubernetes.io/docs/concepts/architecture/cloud-controller/">cloud-controller-manager</a> to monitor the state of our AWS nodes and delete the ones from the Kubernetes database that don&#8217;t exist anymore; but k3s ships with its own, slimmed-down cloud controller manager that has some other nice features, and I didn&#8217;t want to disable that. So instead I just stuffed the cleanup into this post-run hook.</p><h3>Mirkwood</h3><p>Whew! We&#8217;re almost done here! We&#8217;ve got one last git repo to cover before we can have a working messaging app! Mirkwood contains all of my Kubernetes manifests, and it uses <a href="https://kustomize.io/">kustomize</a> to provide the configuration management tooling<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-12" href="#footnote-12" target="_self">12</a>.</p><p>There are (currently) five applications running on my cluster:</p><ol><li><p>The <a href="https://github.com/kubernetes-sigs/aws-ebs-csi-driver/tree/master">AWS EBS CSI driver</a></p></li><li><p>The <a href="https://github.com/aws/aws-node-termination-handler">AWS node termination handler</a></p></li><li><p><a href="https://cert-manager.io/">cert-manager</a></p></li><li><p>The <a href="https://github.com/kubernetes-sigs/external-dns">external-dns</a> operator</p></li><li><p>And, lastly, Prosody itself, aka, the thing we&#8217;ve been trying to run this whole damn time.</p></li></ol><p>The EBS CSI driver watches for Kubernetes <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/">Persistent Volume Claims</a> (PVCs) and handle the creation, deletion, mounting, and unmounting of those block devices into Kubernetes pods. This is necessary so that my XMPP/Prosody pod can have persistent data across pod restarts.  The EBS CSI driver <em>must</em> be running before any pods that request PVCs, so the CSI driver pod tolerates the taint that we created as part of the k3s configuration, and once it&#8217;s running and healthy it removes the taint so that other pods can schedule.</p><p>The node termination handler watches for spot interruption warnings (which AWS provides with a 2-minute time window) and triggers pod and node cleanup ahead of the interruption. This is necessary so that the EBS CSI driver can actually cleanly unmount the EBS volume before all the hardware is rudely yanked out from underneath it<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-13" href="#footnote-13" target="_self">13</a>.</p><p>Cert-manager, as we&#8217;ve discussed, is responsible for talking to Let&#8217;s Encrypt and getting certificates for my XMPP server. It&#8217;s operating using the DNS-01 challenge mode, wherein the Let&#8217;s Encrypt issuer creates a challenge, and then cert-manager creates a TXT record in my public <code>acrl.dev</code> hosted zone to prove that yes, in fact, I do own the zone. Cert-manager then stores the certificate as a secret, which gets injected into the Prosody pod. It also automagically handles getting new certificates ahead of the expiry date, which is pretty slick<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-14" href="#footnote-14" target="_self">14</a>! I&#8217;m a big fan of cert-manger.</p><p>Lastly, external-dns handles setting the DNS entry for my Prosody pod in my private <code>acrl.dev</code> hosted zone. The details here are a little subtle: we create a Kubernetes LoadBalancer Service<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-15" href="#footnote-15" target="_self">15</a>, which <em>normally</em> would be handled by the cloud controller manager to create a network load balancer resource in AWS. But that stuff&#8217;s expensive! We don&#8217;t wanna run no network load balancer here. So instead, we use the builtin <a href="https://docs.k3s.io/networking/networking-services#service-load-balancer">ServiceLB controller</a> in k3s, which more-or-less just exposes the right ports on the host where the pod is running via iptables rules. The external-dns controller just looks up the IP address of the Prosody Service and writes that as an A record into Route53.</p><p>And now we (finally!) have everything we need in order to run our single Kubernetes pod. The Prosody pod requests a 5GB persistent volume to store all of its data, grabs a certificate from cert-manager, uses a ConfigMap to inject all the Prosody configuration, and runs as a single-pod Deployment. And, it all works!</p><h2>Putting all the pieces together</h2><p>There&#8217;s two bits that I didn&#8217;t cover in the above descriptions, which are a) automation and tooling, and b) secrets management. While the details are slightly different for each of my three repos, the high-level bits are the same. For automation and tooling, we use <a href="https://just.systems/">just</a>, which I have increasingly fallen in love with over the last few years. I have justfiles in (almost) every repo, and any time there&#8217;s a complicated command that I need to remember, I drop it into the justfile. So if I needed to recreate this entire setup tomorrow from scratch it would be three commands<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-16" href="#footnote-16" target="_self">16</a>:</p><pre><code><code>~moria &gt; just apply
~isengard &gt; just apply
~mirkwood &gt; just apply</code></code></pre><p>Not quite the one command I started off with in my initial goals, but honestly it&#8217;s pretty good imo. I also have GitHub Actions configured in each of these repos<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-17" href="#footnote-17" target="_self">17</a> to automatically run the above <code>just</code> commands whenever code is changed (or, in the case of moria and isengard, on a periodic schedule to rebuild the AMIs and update the running hosts).</p><p>On the secrets management front, each of the four systems (moria, isengard, mirkwood, and k3s) have slightly different methods for dealing with them. Any secret data is stored encrypted at rest, and the user interface is more-or-less the same for each, it&#8217;s just the backend that changes. Moria uses Pulumi secrets, which are stored encrypted (using local credentials) in the Pulumi state file in S3. Isengard uses Ansible secrets, which are stored encrypted (using local credentials) in the Git repo itself. Mirkwood uses <a href="https://getsops.io/">SOPS</a> to store sensitive data in the Git repo itself, this time encrypted the AWS KMS key that we set up with Moria<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-18" href="#footnote-18" target="_self">18</a>. To make each of these three systems easier to work with, I have <code>just</code> targets configured to read or write encrypted data.</p><p>So that&#8217;s it! After several months of nights-and-weekends work, I now have a usable Kubernetes cluster that is running a single XMPP server in a somewhat reliable fashion, all for about $50/month of AWS bills. Is it cheaper than Slack? No. Is it a better user experience than Slack? Also no. Did I have fun? Lmao, nope, this was annoying as all hell to get configured. Was all the time I spent on this worthwhile, when I could have been using it to make SimKube better? Well&#8230; I think you know where this is going.</p><p>Anyways, hopefully this 4000-word guide (of sorts) might be useful to someone else down the line who is seeking to embark on a similar endeavour. Next time we&#8217;ll be returning to our regularly-scheduled SimKube content!</p><p>As always, thanks for reading.</p><p>~drmorr</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>My words, not his.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>How foolish we are in our youth.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Although I hate the name.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Critics might ask the question, &#8220;right for who?&#8221; and point out that some of the patterns I&#8217;m following are used at giant corporations with entire teams to support them and make sure things don&#8217;t go off the rails, and that I probably ought to be spending less time copying Google and more time hacking on SimKube. My response to that is, I&#8217;m not copying Google, we don&#8217;t do monorepos or Bazel here. Also, if you don&#8217;t like how I do things you&#8217;re free to go start your own company and do things your way instead.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>I <em>strongly</em> debated whether to call this repo &#8220;Mordor&#8221;, but that just didn&#8217;t feel right to me. YAML is kinda gross and there&#8217;s a lot of it, sure, but it&#8217;s not, like, the root of everything evil that is systematically trying to destroy every last bit of beauty and goodness in the world. It&#8217;s more like, you know, just a dark forest with lots of giant spiders.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>As an aside, I spent quite a while trying to figure out what domain name to use for these hosted zones; I wanted something that would be easy to distinguish from my &#8220;public&#8221; domains, namely <code>appliedcomputing.io</code> and <code>simkube.dev</code>. I could use a subdomain and do something like <code>internal.appliedcomputing.io</code> but that is already getting extremely long, and <code>xmpp.internal.appliedcomputing.io</code> is even worse. So I decided it was time to buy a new domain name; I&#8217;m now the proud owner of <code>acrl.dev</code>, which is solely used for internal ACRL services. If you ever see an <code>acrl.dev</code> address in the wild, it means something has gone horribly wrong.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>I thought long and hard about whether to buy <code>acrl.wtf</code> instead of <code>acrl.dev</code> for my internal domain name, but it was $50/year for what is essentially a throw-away domain and that felt like too much.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-8" href="#footnote-anchor-8" class="footnote-number" contenteditable="false" target="_self">8</a><div class="footnote-content"><p>Astute readers will notice that I did <em>briefly</em> mention IAM in the footnotes of the previous post.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-9" href="#footnote-anchor-9" class="footnote-number" contenteditable="false" target="_self">9</a><div class="footnote-content"><p>Please don&#8217;t hack me.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-10" href="#footnote-anchor-10" class="footnote-number" contenteditable="false" target="_self">10</a><div class="footnote-content"><p>The <code>t3.medium</code> and <code>t3.large</code> variants use Intel processors instead of AMD, but are otherwise identical to the <code>t3a</code> variants.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-11" href="#footnote-anchor-11" class="footnote-number" contenteditable="false" target="_self">11</a><div class="footnote-content"><p>Always with the certificates, geez.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-12" href="#footnote-anchor-12" class="footnote-number" contenteditable="false" target="_self">12</a><div class="footnote-content"><p>I mentioned this on Mastodon last week, but the more I learn about Kustomize, the more I don&#8217;t understand why Helm is so popular. Kustomize does everything that Helm does, except better. And it&#8217;s built right in to <code>kubectl</code>! The docs are significantly worse, though, which is probably a big part of the reason why it&#8217;s not used more widely.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-13" href="#footnote-anchor-13" class="footnote-number" contenteditable="false" target="_self">13</a><div class="footnote-content"><p>I mentioned earlier in the post that setting a stable node name for the k3s node caused problems down the line, well, this is one of those places: when the node termination handler activates, it cordons the node, preventing any new pods from scheduling on it. Then the old node goes away and the new node comes up, and k3s looks in its database to see that &#8220;oh huh this node is cordoned, better not let any pods schedule on it!&#8221; So this is how we ended up in the current state. As I&#8217;m writing this, it occurs to me that instead of having my k3s post-start systemd hook clean up the old nodes, I could instead have it just un-cordon the current node; but there were a number of other similar &#8220;stale state&#8221; issues that I was working around with the stable node name that I <em>think</em> the current solution is slightly better. We actually could still end up in a similar situation because AWS uses the node IP address as the hostname, and sometime it will re-use the same IP address for two subsequently-launched nodes; I&#8217;ve seen this happen for large-scale clusters, but I think in my single-node cluster this circumstance seem unlikely.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-14" href="#footnote-anchor-14" class="footnote-number" contenteditable="false" target="_self">14</a><div class="footnote-content"><p>There is a slight issue that I haven&#8217;t figured out how to resolve yet, which is that the Prosody process will need to restart/reload its configuration once it gets the new certificates, but that&#8217;s a problem for 90 days from now, at which point I will have forgotten all of the details about all of this and will therefore spend a day and a half trying to understand why my chat server isn&#8217;t working anymore.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-15" href="#footnote-anchor-15" class="footnote-number" contenteditable="false" target="_self">15</a><div class="footnote-content"><p>&#8220;Service&#8221; is probably the least-well-thought-out name in the entire Kubernetes ecosystem. Literally everyone uses &#8220;service&#8221; to refer to an application that is running out there somewhere in the ether. Kubernetes, however, uses &#8220;service&#8221; to mean very specifically &#8220;the networking configuration that allows other things to talk to the application.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-16" href="#footnote-anchor-16" class="footnote-number" contenteditable="false" target="_self">16</a><div class="footnote-content"><p>I am <em>slightly</em> lying here; I haven&#8217;t fully hooked up all the tooling in each of these repos to apply everything in a single command, so for isengard I would need to apply the bastion configuration and the k3s configuration separately. In mirkwood, I need to <code>just apply</code> each application separately. But don&#8217;t worry! I&#8217;m getting there.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-17" href="#footnote-anchor-17" class="footnote-number" contenteditable="false" target="_self">17</a><div class="footnote-content"><p>Also slightly lying here, I haven&#8217;t set up GitHub Actions for mirkwood yet.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-18" href="#footnote-anchor-18" class="footnote-number" contenteditable="false" target="_self">18</a><div class="footnote-content"><p>Eventually I&#8217;d like to get all three of these repos using KMS instead of relying on local credentials, but, well, there&#8217;s always more work to do, amirite?</p><p></p></div></div>]]></content:encoded></item></channel></rss>