<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Suresh Babu Narayanan</title>
    <description>The latest articles on DEV Community by Suresh Babu Narayanan (@sureshbabunarayanan).</description>
    <link>https://dev.to/sureshbabunarayanan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032905%2F71728ae6-6d15-4b41-a37d-a10a8bec5cf6.png</url>
      <title>DEV Community: Suresh Babu Narayanan</title>
      <link>https://dev.to/sureshbabunarayanan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sureshbabunarayanan"/>
    <language>en</language>
    <item>
      <title>The Kubernetes bill nobody looks at until finance does</title>
      <dc:creator>Suresh Babu Narayanan</dc:creator>
      <pubDate>Mon, 10 Aug 2026 01:00:05 +0000</pubDate>
      <link>https://dev.to/sureshbabunarayanan/the-kubernetes-bill-nobody-looks-at-until-finance-does-3jho</link>
      <guid>https://dev.to/sureshbabunarayanan/the-kubernetes-bill-nobody-looks-at-until-finance-does-3jho</guid>
      <description>&lt;p&gt;Kubernetes does not send you a warning before it quietly costs you three times what it should. It just runs every pod exactly as requested, every replica exactly as configured and the bill arrives a month later as a single number nobody can immediately explain.&lt;/p&gt;

&lt;p&gt;I have spent ten years across AWS, Azure, and Kubernetes (AKS, EKS, ARO) and the pattern behind most "why is our cloud bill so high" conversations is almost never one dramatic mistake. It is four small, boring, easy-to-miss things, quietly compounding across every service in the cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuywvjah9lgqva5yea9lu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuywvjah9lgqva5yea9lu.gif" alt="Shrug" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here they are - with a free calculator and a set of scripts at the end so you can check your own cluster in the next ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core thing to understand: requests, not usage, drive cost
&lt;/h2&gt;

&lt;p&gt;This is the single most important idea in this article, so it goes first: &lt;strong&gt;Kubernetes bills you for what you &lt;em&gt;ask for&lt;/em&gt;, not what you &lt;em&gt;use&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a pod requests &lt;code&gt;500m&lt;/code&gt; CPU and &lt;code&gt;512Mi&lt;/code&gt; memory, the scheduler reserves that exact capacity on a node - permanently, for as long as the pod runs - whether the pod is idle or maxed out. A node's size, and therefore its cost, is determined by the sum of requests scheduled onto it, not by real-time usage graphs.&lt;/p&gt;

&lt;p&gt;This is why a Grafana dashboard showing "12% average CPU utilization" and a cloud bill that keeps climbing aren't in tension. They're both true, and they're describing the same problem from two different angles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1 - requests set once, in a hurry, and never revisited
&lt;/h2&gt;

&lt;p&gt;Someone writes the first version of a deployment manifest. They don't know the real usage pattern yet, because the service doesn't exist yet, so they guess generously - "let's give it 1 CPU and 1Gi, we can tune later." It ships. It works. Nobody tunes it later, because it's working, and "working" doesn't create a ticket.&lt;/p&gt;

&lt;p&gt;Multiply that single generous guess across every microservice a team ships, and you get a cluster where every workload is sized for a worst case that rarely arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix isn't guessing better - it's measuring.&lt;/strong&gt; Run &lt;code&gt;kubectl top pods&lt;/code&gt; against your actual traffic for a week or two, then compare it to requests. The gap is your first real answer to "where does our cloud spend actually go."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pods &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
NAME:.metadata.name,CPU_REQ:.spec.containers[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;.resources.requests.cpu,MEM_REQ:.spec.containers[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;.resources.requests.memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kfxlbjstoohq4n23sc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kfxlbjstoohq4n23sc3.png" alt=" " width="799" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2 - orphaned storage and empty load balancers
&lt;/h2&gt;

&lt;p&gt;These are the sneakiest of the four, because they don't show up in CPU or memory dashboards at all. They're not compute problems - they're forgotten housekeeping.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;PersistentVolumeClaim&lt;/code&gt; survives its pod. Delete the deployment, forget the PVC existed, and the underlying disk keeps being billed every month, silently, until someone audits storage specifically. A &lt;code&gt;LoadBalancer&lt;/code&gt;-type Service can end up with zero backing endpoints - the pods behind it were scaled down or deleted, but the cloud load balancer it provisioned is still running and still billing, whether or not a single packet reaches it.&lt;/p&gt;

&lt;p&gt;Neither shows up as "high CPU" or "high memory." They show up as a line item nobody remembers approving.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxsjiokqjgjeu4yrtrpf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxsjiokqjgjeu4yrtrpf.gif" alt="where did my money go" width="480" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PVCs not currently mounted by any pod&lt;/span&gt;
kubectl get pvc &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt;

&lt;span class="c"&gt;# LoadBalancer services worth checking for zero endpoints&lt;/span&gt;
kubectl get svc &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; spec.type&lt;span class="o"&gt;=&lt;/span&gt;LoadBalancer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The toolkit at the end automates the actual "is this one orphaned" comparison - doing it by hand across a big cluster gets tedious fast.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3 - non-prod environments running like it's prod
&lt;/h2&gt;

&lt;p&gt;Dev and staging namespaces get created by copying the closest working example - which is usually the production manifest, replica count and all. Nobody consciously decided "this dev environment should run 3 replicas of everything, 24 hours a day, 7 days a week, including the entire weekend when literally nobody is looking at it." It just inherited that shape from prod and nobody revisited the decision.&lt;/p&gt;

&lt;p&gt;Run at business hours only (roughly 45 hours/week) instead of continuously (168 hours/week), and a non-prod environment can cost well under half of its always-on price - often closer to a quarter - for identical infrastructure. A scheduled scale-to-zero is usually a single CronJob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scale-down-dev&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;19&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1-5"&lt;/span&gt;   &lt;span class="c1"&gt;# 7pm weekdays&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl&lt;/span&gt;
              &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitnami/kubectl&lt;/span&gt;
              &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubectl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scale"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployment"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--all"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--replicas=0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-n"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Pair it with a matching scale-up CronJob for 8am, obviously - nobody wants to explain to the team why dev is down every morning.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4 - no autoscaling, so peak-sizing runs all day
&lt;/h2&gt;

&lt;p&gt;Without a &lt;code&gt;HorizontalPodAutoscaler&lt;/code&gt;, a deployment runs at whatever fixed replica count someone typed once - usually sized for the &lt;em&gt;worst&lt;/em&gt; moment it needs to handle, running continuously even during the other 20 hours a day when traffic is a fraction of that peak.&lt;/p&gt;

&lt;p&gt;The fix is one resource, and it pays for itself continuously once it exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Resource&lt;/span&gt;
      &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
          &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this actually costs - a worked example
&lt;/h2&gt;

&lt;p&gt;Take a genuinely ordinary microservice: 500m CPU and 512Mi memory requested, 3 replicas, running at roughly 35% real average utilization - not a horror story, just a typical unreviewed service. At AWS on-demand list pricing, that's roughly &lt;strong&gt;$35-40/month requested&lt;/strong&gt;, of which &lt;strong&gt;roughly $23-26/month sits unused&lt;/strong&gt; at that utilization level.&lt;/p&gt;

&lt;p&gt;One service, ~$25/month wasted. Doesn't sound alarming. Now multiply it across the 40, 80, or 200 microservices a mid-sized platform team actually runs, and the number stops being trivial - it becomes the kind of line item a finance team asks about by name.&lt;/p&gt;

&lt;p&gt;I built a small calculator so you can plug in your own numbers instead of trusting mine: &lt;strong&gt;&lt;a href="https://dev.toTOOL%20LINK"&gt;PodTab →&lt;/a&gt;&lt;/strong&gt; - type in requests, replicas, and real usage percentage, get an itemized monthly and annual figure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmracpwjlr9ziv3ghwpmb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmracpwjlr9ziv3ghwpmb.png" alt=" " width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own cluster in the next ten minutes
&lt;/h2&gt;

&lt;p&gt;Everything above is diagnosable with &lt;code&gt;kubectl&lt;/code&gt; alone - no new tooling, no agent installed in your cluster, nothing sent anywhere. I packaged the checks I run into three small scripts: one flags over-provisioned pods against real usage, one finds orphaned PVCs and empty load balancers, one finds deployments with no autoscaler and non-prod namespaces quietly running at prod scale.&lt;/p&gt;

&lt;p&gt;Free version: build the checks yourself from the &lt;code&gt;kubectl&lt;/code&gt; commands above - everything you need is in this article. Packaged version, if you'd rather not assemble it: &lt;strong&gt;&lt;a href="https://dev.toTOOLKIT%20LINK"&gt;Kubernetes Cost Audit Toolkit →&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Either way - go check. The waste in most clusters isn't dramatic. It's four small, boring things, quietly compounding, waiting for someone to actually look.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a DevOps engineer with 13 years across AWS, Azure, Kubernetes, Terraform and Ansible. Previously: &lt;a href="https://dev.toARTICLE-2-LINK"&gt;Terraform state mistakes&lt;/a&gt; and &lt;a href="https://dev.toARTICLE-1-LINK"&gt;a self-healing pipeline with Prometheus&lt;/a&gt;. I also maintain &lt;a href="https://cronport.netlify.app" rel="noopener noreferrer"&gt;CronPort&lt;/a&gt;, a free cron expression converter, and sell a &lt;a href="https://sureshdevops.gumroad.com/l/terraform-aws-starter-kit" rel="noopener noreferrer"&gt;Terraform AWS Starter Kit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>devops</category>
      <category>finops</category>
    </item>
    <item>
      <title>10 Hard-Earned Lessons from a Year of Building with LLMs in Production</title>
      <dc:creator>Suresh Babu Narayanan</dc:creator>
      <pubDate>Wed, 05 Aug 2026 05:35:38 +0000</pubDate>
      <link>https://dev.to/sureshbabunarayanan/10-hard-earned-lessons-from-a-year-of-building-with-llms-in-production-akc</link>
      <guid>https://dev.to/sureshbabunarayanan/10-hard-earned-lessons-from-a-year-of-building-with-llms-in-production-akc</guid>
      <description>&lt;p&gt;AI Twitter (and LinkedIn, and every tech newsletter) makes LLM engineering look like a solved problem: wire up an API call, add a system prompt, ship it. Anyone who's actually taken an LLM feature from prototype to production knows that's about 10% of the job. The other 90% is the unglamorous stuff - the failure modes nobody demos, the costs nobody mentions in the launch tweet, the "it worked great until real users touched it" moments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia1.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExejRleHd6bWJqOHFyYnEzdDR4cXdsbXdmb2Fzd3dkOTVvZ3FhMDY5biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F75ZaxapnyMp2w%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia1.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExejRleHd6bWJqOHFyYnEzdDR4cXdsbXdmb2Fzd3dkOTVvZ3FhMDY5biZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F75ZaxapnyMp2w%2Fgiphy.gif" alt="mind blown" width="200" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are ten lessons that took me longer to learn than they should have, and that I wish someone had told me before I started.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your prompt is a piece of software, not a paragraph&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Early on, prompts feel like writing - you tweak the wording until the output looks right, and move on. That mindset falls apart fast once a prompt has to survive contact with real, messy user input.&lt;/p&gt;

&lt;p&gt;Treat prompts like code: version them, test them against a fixed set of inputs before every change, and track regressions. A prompt that works beautifully on your five test cases and silently breaks on the sixth isn't done - it's untested.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Evaluation is the actual hard part&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building the feature is fast. Knowing whether it's good is slow. Most teams underinvest here because eval work doesn't produce a demo-able artifact - it produces confidence, and confidence doesn't screenshot well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZzdjZzBndGNlbjYxNnc5a2N4c2hzcTN2eHY1ZmZ0eXowOXQ5ZXVtNiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F11kTQgng5gbqgM%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZzdjZzBndGNlbjYxNnc5a2N4c2hzcTN2eHY1ZmZ0eXowOXQ5ZXVtNiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F11kTQgng5gbqgM%2Fgiphy.gif" alt="nodding" width="245" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix isn't complicated, just unglamorous: build a small, real eval set from actual (or realistic) inputs early, and re-run it every time you touch a prompt, model, or pipeline step. Even 30-50 well-chosen examples beat vibes-based testing by a wide margin.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieval quality matters more than model choice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building anything RAG-adjacent, it's tempting to assume a better model will fix bad answers. In practice, most "the AI is wrong" bugs I've debugged were retrieval bugs wearing a model-quality costume - the right chunk of context simply never made it into the prompt.&lt;/p&gt;

&lt;p&gt;Before swapping models, check what your retrieval step is actually returning. Log it. Read it. It's usually the culprit.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Structured output is worth the upfront pain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Free-form text output feels faster to build, until you're three regex patterns deep trying to parse a response that almost-but-not-quite follows the format you asked for. Investing in structured output (function calling, JSON schemas, whatever your provider supports) early saves you from a category of bugs that otherwise creeps in right before launch.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Latency is a feature, not an afterthought&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users tolerate a slow first response far less than a slow search result, because they expect a "chat" to feel conversational. Streaming responses, showing intermediate steps for agentic workflows, and setting realistic timeouts aren't polish - they're core to whether the feature feels usable at all.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cost scales in ways that surprise you&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A demo costing a few cents per call feels irrelevant, right up until it's running at production volume with retries, multi-step agent loops, and users who send far more requests than your test users ever did. Model cost, retry cost, and "the agent looped four times before finishing" cost are three different line items - track them separately, or the invoice will surprise you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExMm1lN284NDhpbjlmbndmbjhkMjFkeXRrZWN4anZydnA1eXQwZ2xsbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FQ8JhKDsvVxPRS%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExMm1lN284NDhpbjlmbndmbjhkMjFkeXRrZWN4anZydnA1eXQwZ2xsbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FQ8JhKDsvVxPRS%2Fgiphy.gif" alt="wallet empty" width="320" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Guardrails aren't optional, even for "internal tools"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's tempting to skip input/output filtering on internal or low-stakes tools. Then someone pastes something unexpected into the input field, or an agent with tool access does something you didn't anticipate, and you're debugging an incident instead of shipping a feature. Basic guardrails scale down easily; retrofitting them after an incident doesn't.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agentic workflows fail differently than single-shot calls&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A single bad response is annoying. A multi-step agent that makes a wrong decision early and confidently builds on it for five more steps is a much harder problem - the failure compounds instead of staying contained.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExdzlzaTFpb293ajdlNTlvZTc5MWl5Y2tuMWF3cHl2NjY5NWQ2eGtoOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FZD8ZjehSsLDZQRKJjJ%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExdzlzaTFpb293ajdlNTlvZTc5MWl5Y2tuMWF3cHl2NjY5NWQ2eGtoOCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FZD8ZjehSsLDZQRKJjJ%2Fgiphy.gif" alt="chaos" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building agents (and it feels like everyone is right now), invest early in step-level logging and the ability to see where a chain went wrong, not just that it did.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MCP and tool ecosystems are changing the integration story fast&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Model Context Protocol and similar standards are quietly solving a problem that used to eat weeks of custom integration work per tool. If you haven't looked at how MCP-style tool servers work recently, it's worth revisiting — the ecosystem is moving fast enough that assumptions from six months ago are already outdated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The best LLM feature is often the smallest one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The instinct is to build the most impressive agentic pipeline possible. The features that actually stick with users tend to be narrower and more reliable: one job, done well, with predictable behavior - not a general-purpose agent that's occasionally magical and occasionally wrong in ways users can't predict.&lt;/p&gt;

&lt;p&gt;None of this is meant to be discouraging - it's the opposite. LLM engineering is still young enough that most of these lessons come from hands-on scar tissue rather than established best practice. If you're early in this space, the gap between "the demo worked" and "it works reliably for real users" is exactly where the interesting engineering happens.&lt;/p&gt;

&lt;p&gt;What's the lesson that took you the longest to learn? I'd genuinely like to know - drop it in the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZHd2amUyNzIzemVpbGtyNTBzYmx4anVydWhqeTI2dTQwNWFvdTBxMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2Fwrzf9P70YWLJK%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZHd2amUyNzIzemVpbGtyNTBzYmx4anVydWhqeTI2dTQwNWFvdTBxMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2Fwrzf9P70YWLJK%2Fgiphy.gif" alt="high five" width="380" height="214"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a DevOps engineer with 10 years across AWS, Azure, Kubernetes, Terraform and Ansible. Previously: &lt;a href="https://dev.to/sureshbabunarayanan/my-nginx-died-on-a-saturday-so-i-taught-it-to-fix-itself-4bo1"&gt;I built a self-healing pipeline with Prometheus and 50 lines of Python&lt;/a&gt;. I also maintain &lt;a href="https://cronport.netlify.app" rel="noopener noreferrer"&gt;CronPort&lt;/a&gt;, a free cron expression converter for crontab, Kubernetes, GitHub Actions, AWS EventBridge and Terraform.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>productivity</category>
    </item>
    <item>
      <title>10 years in DevOps, 2 weeks on DEV, and a deploy queue that humbled me</title>
      <dc:creator>Suresh Babu Narayanan</dc:creator>
      <pubDate>Tue, 28 Jul 2026 05:14:44 +0000</pubDate>
      <link>https://dev.to/sureshbabunarayanan/10-years-in-devops-2-weeks-on-dev-and-a-deploy-queue-that-humbled-me-2oia</link>
      <guid>https://dev.to/sureshbabunarayanan/10-years-in-devops-2-weeks-on-dev-and-a-deploy-queue-that-humbled-me-2oia</guid>
      <description>&lt;p&gt;Hi. I'm Suresh - DevOps/automation engineer, 10 years in, currently deep in AWS, Azure, Kubernetes, Terraform, and Ansible for a living. I joined DEV two weeks ago. I've published two articles. I have no idea what I'm doing with this platform yet, and I'm having a genuinely great time finding out.&lt;/p&gt;

&lt;p&gt;This isn't a "here's what I learned" listicle. It's more like - pull up a chair, let me tell you what the last two weeks looked like from the inside, because some of it was funnier than I expected infrastructure writing to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why start now, after 10 years of &lt;em&gt;not&lt;/em&gt; writing
&lt;/h2&gt;

&lt;p&gt;Short version: I got tired of solving the same problem quietly, for the fifth company, and having literally nothing to show for it except my own memory. Thirteen years of pattern-matching - "oh, this is the local-state-on-a-laptop problem again," "oh, this is the giant-shared-state-file problem again" - and none of it existed anywhere I could point to. It just lived in my head, useful to exactly one person.&lt;/p&gt;

&lt;p&gt;So: write it down. Worst case, nobody reads it and I've still got a clean writeup of things I already knew. Best case, someone googling the exact 2am problem I've already solved five times finds it and skips the pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened in two weeks
&lt;/h2&gt;

&lt;p&gt;I built a self-healing Prometheus + Alertmanager pipeline that restarts nginx on its own, wrote it up, and - this is the part nobody warns you about - &lt;strong&gt;spent longer getting the cover image and Dev.to formatting right than writing the actual article.&lt;/strong&gt; Nobody tells you that going in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxh2m16moptbwfatn85od.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxh2m16moptbwfatn85od.gif" alt="this is fine" width="480" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I wrote about the five Terraform state mistakes I've watched happen (and caused, once, early on - we don't need to relive that one in detail). Somewhere in the middle of publishing that one, I also deployed a small free tool, and this is the actual highlight of my two weeks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deploy got stuck. Queued. For twenty minutes. Then I retried, and it queued &lt;em&gt;again&lt;/em&gt;.&lt;/strong&gt; Turns out it was a known platform-side issue completely outside my control - nothing wrong with my file, nothing wrong with my setup, just a stuck queue with no ETA. Thirteen years of infrastructure experience, and my solution was the same one I'd tell anyone else: stop waiting, fail over. Dragged the exact same file onto a different host. Live in about ninety seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb7o3yjlj8y28rwxr0s4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb7o3yjlj8y28rwxr0s4.gif" alt="plot twist" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The irony was not lost on me that I'd just published an article about &lt;em&gt;self-healing infrastructure&lt;/em&gt; the same week my own deploy needed a very manual, very human intervention to heal itself. Infrastructure has a sense of humor. It is not always kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual surprises, ranked
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Writing the article is maybe 40% of the work.&lt;/strong&gt; Cover image, tags, formatting, screenshots, re-reading it four times to make sure it actually sounds like me and not like a template - that's the other 60%. I did not budget for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People are nicer than I expected.&lt;/strong&gt; I came in braced for the stereotypical "well actually" comment section. Hasn't happened yet. Might just be early days, but so far it's been genuinely constructive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "did I really live this or does it just sound made up" test is real.&lt;/strong&gt; I made myself a rule early on: if I can't answer a specific follow-up question about something I wrote, I don't publish it as my own story. Turns out that's a surprisingly high bar when you're staring at a blank editor at 11pm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I have way more stories than I thought.&lt;/strong&gt; Thirteen years apparently generates a lot of "oh, that reminds me of the time..." - I just never had a reason to write any of them down before.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;More of the same, honestly. Real problems I've actually hit, real fixes, occasionally a tool if I get annoyed enough at doing something manually more than twice. No hot takes on whatever's trending this week unless I've genuinely got something to add.&lt;/p&gt;

&lt;h2&gt;
  
  
  AMA, kind of
&lt;/h2&gt;

&lt;p&gt;Two weeks isn't enough runway for a proper "ask me anything," but I'll happily answer anything about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps/platform engineering as a 10-year career (AWS, Azure, Kubernetes, the lot)&lt;/li&gt;
&lt;li&gt;What it's actually like starting a technical writing habit from zero, in real time, badly, alongside a full-time job&lt;/li&gt;
&lt;li&gt;The self-healing pipeline or Terraform state posts, if you've read either&lt;/li&gt;
&lt;li&gt;Whether infrastructure really does have a sense of humor, or if I'm just tired&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExM253c2VxdGE2YXp2Y2RoM25nN3p5Ymg1bmYydmx4dnY1cGJkOWNlNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F26gsbDPvdV5dTZAQM%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExM253c2VxdGE2YXp2Y2RoM25nN3p5Ymg1bmYydmx4dnY1cGJkOWNlNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F26gsbDPvdV5dTZAQM%2Fgiphy.gif" alt="AMA" width="480" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a comment - genuinely, I'll answer everything. Two weeks in, this is still the part I didn't expect to enjoy as much as I do.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>career</category>
      <category>discuss</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Five Terraform state mistakes I keep seeing after 10 years (and the fixes)</title>
      <dc:creator>Suresh Babu Narayanan</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:56:48 +0000</pubDate>
      <link>https://dev.to/sureshbabunarayanan/five-terraform-state-mistakes-i-keep-seeing-after-13-years-and-the-fixes-4kc6</link>
      <guid>https://dev.to/sureshbabunarayanan/five-terraform-state-mistakes-i-keep-seeing-after-13-years-and-the-fixes-4kc6</guid>
      <description>&lt;p&gt;Terraform's state file is the most important file most teams never think about - right up until the day it thinks about them.&lt;/p&gt;

&lt;p&gt;I've spent thirteen years in infrastructure, the last several deep in Terraform across AWS and Azure, and the same five state mistakes keep appearing. Not in beginner projects - in real companies, with real change boards, usually discovered at the worst possible moment. Every one of them is boring to fix &lt;em&gt;before&lt;/em&gt; it happens and expensive to fix after.&lt;/p&gt;

&lt;p&gt;Here they are, with the fixes I now bake into every project on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1 - Local state on someone's laptop
&lt;/h2&gt;

&lt;p&gt;The default &lt;code&gt;terraform.tfstate&lt;/code&gt; sitting in the project folder works perfectly for exactly one person, for exactly as long as their laptop lives. Then a second engineer joins, or the laptop dies, and you discover that the state file &lt;em&gt;is&lt;/em&gt; the infrastructure's memory. Lose it, and Terraform looks at your cloud account like it's never met it before.&lt;/p&gt;

&lt;p&gt;I've seen this exact scenario play out early in a project, before anyone had gotten around to setting up a proper backend. State living locally, on one engineer's laptop, "for now." Then a re-clone, or a laptop swap, or just someone else needing to run a plan - and the moment of "wait, where's the state file?" that makes everyone's stomach drop slightly. Nothing was permanently lost in the end, but it's the kind of near-miss that makes you migrate to remote state on every project since, on day one, no exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; remote state in S3, from the first commit, with versioning and encryption on. The bucket is a five-minute bootstrap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state_bucket_name&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;prevent_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# deleting your state bucket is a very bad day&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;prevent_destroy&lt;/code&gt; line has saved more weekends than any monitoring tool I've deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2 - No state locking
&lt;/h2&gt;

&lt;p&gt;Two engineers run &lt;code&gt;terraform apply&lt;/code&gt; at the same time. Both read the same state, both write different endings to it. The result is a state file that matches neither reality nor anyone's intentions - Terraform's version of a git merge conflict, except the conflict is your production VPC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix used to be&lt;/strong&gt; a DynamoDB table, and half the tutorials online still say so. But since Terraform 1.11, the S3 backend can lock natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"yourco-tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-2"&lt;/span&gt;
    &lt;span class="nx"&gt;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# S3-native locking - no DynamoDB table needed&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One less resource to bootstrap, pay for, and explain to auditors. If you're pinned below 1.11, keep DynamoDB - but put a version upgrade on the roadmap, because this is genuinely simpler.&lt;/p&gt;

&lt;p&gt;![How S3-native locking resolves the race - the second apply waits, nothing corrupts]&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxuce3elxbhmjehgfgoxd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxuce3elxbhmjehgfgoxd.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The second &lt;code&gt;apply&lt;/code&gt; doesn't corrupt anything or silently overwrite - it just waits its turn.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3 - One giant state file for everything
&lt;/h2&gt;

&lt;p&gt;Dev, staging, prod, the DNS zones, that one Lambda from 2023 - all in a single state. Every &lt;code&gt;plan&lt;/code&gt; takes minutes and touches everything. Every mistake has maximum blast radius. Every junior engineer's experiment holds a loaded reference to prod.&lt;/p&gt;

&lt;p&gt;I've worked in environments where dev, staging, and prod infrastructure lived far too close together - sometimes sharing state, sometimes just sharing enough blast radius that a &lt;code&gt;plan&lt;/code&gt; in one environment made everyone nervous about the others. Plans that should take seconds stretch out uncomfortably long when Terraform has to reason about everything at once, and every review turns into "wait, what else does this touch?" It's a slow tax that nobody notices being introduced and everybody complains about once it's there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; one state file per environment, minimum. Same code, different backends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;envs/
├── dev/    → key = "dev/terraform.tfstate"    (2 AZs, NAT off - cheap)
└── prod/   → key = "prod/terraform.tfstate"   (3 AZs, NAT on)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The objection is always "but then dev and prod can drift apart." They can - and that's a code review problem, which is a much better problem than "dev corrupted prod's state," which is a résumé problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4 - Treating state as too sacred to touch (or not sacred enough)
&lt;/h2&gt;

&lt;p&gt;Two opposite failure modes, same root cause: nobody on the team actually knows the state manipulation commands.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;not sacred enough&lt;/em&gt; version: someone opens &lt;code&gt;terraform.tfstate&lt;/code&gt; in an editor and hand-edits JSON. I have watched this happen. It works right up until it catastrophically doesn't.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;too sacred&lt;/em&gt; version: a resource needs renaming, and because nobody trusts themselves with state surgery, the team does a destroy-and-recreate of a production database instead. The scheduled-downtime email goes out for what should have been a zero-downtime metadata change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; learn three tools, in this order -&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# renaming/refactoring a resource without touching the real thing:&lt;/span&gt;
terraform state &lt;span class="nb"&gt;mv &lt;/span&gt;aws_instance.web aws_instance.web_server

&lt;span class="c"&gt;# adopting an existing resource into Terraform (1.5+, plan-reviewable):&lt;/span&gt;
import &lt;span class="o"&gt;{&lt;/span&gt;
  to &lt;span class="o"&gt;=&lt;/span&gt; aws_s3_bucket.legacy
  &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-existing-bucket"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# removing something from state WITHOUT destroying it (1.7+):&lt;/span&gt;
removed &lt;span class="o"&gt;{&lt;/span&gt;
  from &lt;span class="o"&gt;=&lt;/span&gt; aws_instance.snowflake
  lifecycle &lt;span class="o"&gt;{&lt;/span&gt; destroy &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;removed&lt;/code&gt; blocks are the underrated ones - they turn state surgery into a reviewable pull request instead of a hero typing commands into prod at 6pm.&lt;/p&gt;

&lt;p&gt;I've had to reach for &lt;code&gt;terraform import&lt;/code&gt; and manual state inspection more than once - usually to bring an existing resource under management without Terraform deciding the "safe" move was to destroy and recreate it. That instinct - trusting &lt;code&gt;state mv&lt;/code&gt; and &lt;code&gt;import&lt;/code&gt; over letting Terraform tear something down "because state says so" - is something you only really learn by almost letting the destroy happen once and catching it in the plan output first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 5 - Secrets in state, state in Git
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth many teams learn during their first security review: Terraform state stores resource attributes &lt;em&gt;in plaintext&lt;/em&gt; - including database passwords, and anything a provider returns. Which means the day someone commits &lt;code&gt;terraform.tfstate&lt;/code&gt; to the repo "just as a backup," those secrets are in Git history forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is layered:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; the state files on day one - &lt;code&gt;*.tfstate&lt;/code&gt; and &lt;code&gt;*.tfstate.*&lt;/code&gt; (backup files too)&lt;/li&gt;
&lt;li&gt;Encrypt the state bucket (KMS server-side encryption in the bootstrap above)&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;ephemeral&lt;/code&gt; values and secret-manager references over raw secret strings in configuration, so fewer secrets land in state at all&lt;/li&gt;
&lt;li&gt;Treat state bucket access as production access - IAM-restricted, logged&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're checking one thing after reading this article, make it &lt;code&gt;git log --all -- "*.tfstate"&lt;/code&gt; on your oldest repo. I hope it comes back empty. It often doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns you'll probably recognise, even if the stories above don't quite match
&lt;/h2&gt;

&lt;p&gt;State mistakes tend to rhyme across companies more than people expect. A few more I'd bet you've seen at least one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform.tfstate&lt;/code&gt; committed to Git "just this once" - and now it's in history forever&lt;/li&gt;
&lt;li&gt;Two people running &lt;code&gt;apply&lt;/code&gt; in the same environment at the same time, no lock, no warning until the diff looks wrong&lt;/li&gt;
&lt;li&gt;A renamed resource treated as delete-then-create instead of a &lt;code&gt;state mv&lt;/code&gt;, because nobody knew the command existed&lt;/li&gt;
&lt;li&gt;Local state surviving purely because nobody's laptop has died yet - not a policy, just luck&lt;/li&gt;
&lt;li&gt;State quietly holding secrets nobody remembered were sensitive, until a security review found them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If even one of these lands, you already know exactly which of the five fixes above to reach for first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern behind all five
&lt;/h2&gt;

&lt;p&gt;None of these mistakes are exotic. They're all the same mistake wearing five costumes: &lt;strong&gt;treating state as an implementation detail instead of what it actually is - the single source of truth that makes Terraform work at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The teams that avoid the 2am state incidents aren't smarter. They just made five boring decisions on day one: remote backend, locking, per-env isolation, learned state surgery, and secrets hygiene. Thirty minutes of setup, total.&lt;/p&gt;

&lt;h2&gt;
  
  
  I packaged the thirty minutes
&lt;/h2&gt;

&lt;p&gt;Because I kept setting up the same foundations on every new engagement, I turned my day-one layout into a small kit: bootstrap for a versioned/encrypted/locked state bucket, per-environment structure with S3-native locking, a multi-AZ VPC module, provider-level &lt;code&gt;default_tags&lt;/code&gt; so untagged resources are impossible, and pre-commit hooks (fmt, validate, tflint) so bad code never reaches CI.&lt;/p&gt;

&lt;p&gt;The free version of the knowledge is this article - you can build all of it yourself from what's above. If you'd rather skip the assembly, the kit is here: &lt;strong&gt;&lt;a href="https://sureshdevops.gumroad.com/l/terraform-aws-starter-kit" rel="noopener noreferrer"&gt;Terraform AWS Starter Kit&lt;/a&gt;&lt;/strong&gt; - and the repo layout is documented in the listing so you know exactly what you're getting.&lt;/p&gt;

&lt;p&gt;Either way: go check where your state lives. Today, ideally.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a DevOps engineer with 10 years across AWS, Azure, Kubernetes, Terraform and Ansible. Previously: &lt;a href="https://dev.to/sureshbabunarayanan/my-nginx-died-on-a-saturday-so-i-taught-it-to-fix-itself-4bo1"&gt;I built a self-healing pipeline with Prometheus and 50 lines of Python&lt;/a&gt;. I also maintain &lt;a href="https://cronport.netlify.app" rel="noopener noreferrer"&gt;CronPort&lt;/a&gt;, a free cron expression converter for crontab, Kubernetes, GitHub Actions, AWS EventBridge and Terraform.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My nginx died on a Saturday, so I taught it to fix itself</title>
      <dc:creator>Suresh Babu Narayanan</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:26:44 +0000</pubDate>
      <link>https://dev.to/sureshbabunarayanan/my-nginx-died-on-a-saturday-so-i-taught-it-to-fix-itself-4bo1</link>
      <guid>https://dev.to/sureshbabunarayanan/my-nginx-died-on-a-saturday-so-i-taught-it-to-fix-itself-4bo1</guid>
      <description>&lt;p&gt;A few months ago I set myself a weekend challenge: could I make a service restart itself when it dies, using only free open-source tools, with zero manual steps in between?&lt;/p&gt;

&lt;p&gt;Not because restarting nginx is hard. It's one command. But I've spent thirteen years in infrastructure, and the pattern I keep seeing is this: the alert fires at 2am, a human wakes up, squints at a phone, VPNs in, and runs the one command. The gap between "we knew" and "we acted" is where the pain lives. I wanted to close that gap on my own laptop, end to end, so I properly understood every link in the chain - not just the theory.&lt;/p&gt;

&lt;p&gt;Here's the whole thing: Prometheus notices nginx is down, Alertmanager fires a webhook, and a small Python service restarts the container. Total cost: $0. Total code I had to write: about 50 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx  →  nginx-exporter  →  Prometheus  →  Alertmanager  →  responder (Flask)  →  docker restart nginx
         (status→metrics)    (scrape+rule)   (route alert)     (receive+act)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqz4xszv1mn05sq5xtmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqz4xszv1mn05sq5xtmk.png" alt=" " width="700" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five containers, one docker-compose file. The full repo is at the end - everything below is copy-pasteable, but I'd genuinely suggest typing the alert rule and the responder yourself. That's where the learning is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; a laptop with Docker, basic YAML tolerance, and about an hour. It took me longer the first time, and I'll show you exactly where I lost the time, because that part taught me more than the parts that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - nginx and its exporter
&lt;/h2&gt;

&lt;p&gt;nginx doesn't speak Prometheus natively, so we pair it with the official exporter, which reads nginx's &lt;code&gt;stub_status&lt;/code&gt; page and republishes it as metrics - including the one we care about: &lt;code&gt;nginx_up&lt;/code&gt;, which is 1 when nginx answers and 0 when it doesn't.&lt;/p&gt;

&lt;p&gt;The nginx config just adds a status listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/stub_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;stub_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in docker-compose, the exporter points at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;nginx-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx/nginx-prometheus-exporter:1.1.0&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--nginx.scrape-uri=http://nginx:8081/stub_status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why an exporter instead of just letting Prometheus scrape nginx directly? Because Prometheus's own &lt;code&gt;up&lt;/code&gt; metric tells you whether the &lt;em&gt;scrape target&lt;/em&gt; answered - and if you scrape the exporter, &lt;code&gt;up&lt;/code&gt; stays 1 even while nginx is dead, because the exporter itself is fine. &lt;code&gt;nginx_up&lt;/code&gt; is the truth. This distinction bit me once in a real environment years ago, and it's the kind of thing you only internalise by getting it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 - Prometheus and the alert rule
&lt;/h2&gt;

&lt;p&gt;Prometheus scrapes the exporter every 5 seconds (aggressive, but this is a demo and I wanted fast feedback loops):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alert rule is four lines that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NginxDown&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx_up == &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto_restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;for: 30s&lt;/code&gt; is doing more work than it looks like. Without it, a single blipped scrape - a container pausing for GC, a network hiccup - fires the alert and restarts a perfectly healthy service. With it, nginx has to be &lt;em&gt;continuously&lt;/em&gt; down for 30 seconds before anything happens. Every flapping-alert horror story I've heard traces back to someone skipping this line. In production I'd set it longer; for a demo, 30 seconds keeps the feedback loop tight.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;action: auto_restart&lt;/code&gt; label is a habit from working with event-driven automation at scale: route on labels, not alert names. Today one alert triggers a restart; later, twenty alerts can share the same remediation path just by carrying the label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Alertmanager, and where I lost an hour
&lt;/h2&gt;

&lt;p&gt;Alertmanager's job is routing: this alert goes to that webhook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto-remediation&lt;/span&gt;
  &lt;span class="na"&gt;group_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alertname"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;group_wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
  &lt;span class="na"&gt;repeat_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;

&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto-remediation&lt;/span&gt;
    &lt;span class="na"&gt;webhook_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://responder:5000/alert&lt;/span&gt;
        &lt;span class="na"&gt;send_resolved&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where my Saturday went sideways. My first version had the webhook URL pointing at &lt;code&gt;localhost:5000&lt;/code&gt; - which inside the Alertmanager container means &lt;em&gt;Alertmanager itself&lt;/em&gt;, not my responder. Prometheus showed the alert firing, Alertmanager showed it received, and… nothing happened. No errors anywhere obvious, because from Alertmanager's point of view nothing was wrong; it was dutifully delivering webhooks into the void.&lt;/p&gt;

&lt;p&gt;The fix is obvious in hindsight - use the compose service name, &lt;code&gt;responder:5000&lt;/code&gt; - but I mention it because "alert fires, action doesn't happen, no visible error" is exactly the failure mode you must know how to debug before trusting automation to act for you. &lt;code&gt;docker logs alertmanager&lt;/code&gt; and a &lt;code&gt;curl&lt;/code&gt; from inside the container found it in the end.&lt;/p&gt;

&lt;p&gt;The other setting worth a sentence: &lt;code&gt;repeat_interval: 5m&lt;/code&gt; means if the alert keeps firing, the webhook re-fires every 5 minutes. That sounds helpful until you realise it means your restart action will retry forever against a service that's &lt;em&gt;never&lt;/em&gt; coming back. Hold that thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - the responder: 50 lines of Flask
&lt;/h2&gt;

&lt;p&gt;The receiving end is deliberately boring - a Flask app that validates the alert and restarts the container via the Docker API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alerts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alertname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NginxDown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;allowed_to_restart&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;circuit breaker OPEN - escalate to a human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;restart_history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I care about is &lt;code&gt;allowed_to_restart()&lt;/code&gt; - a circuit breaker that refuses more than 3 restarts in 10 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allowed_to_restart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;restart_history&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;restart_history&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WINDOW_SECONDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;restart_history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restart_history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_RESTARTS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother, in a toy? Because auto-remediation without a stop condition isn't self-healing, it's self-harm with good intentions. If nginx is crashing because of a broken config or a full disk, restarting it in a loop fixes nothing and hides the real problem behind a wall of "remediation succeeded" logs. Three strikes, then a human gets paged. In a real setup this is where you'd escalate to Slack or PagerDuty; in the demo it just logs loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - kill it and watch it heal
&lt;/h2&gt;

&lt;p&gt;The payoff. With everything running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I sat and watched three windows: the Prometheus alerts page, &lt;code&gt;docker logs -f responder&lt;/code&gt;, and &lt;code&gt;docker ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sequence: the alert went &lt;em&gt;pending&lt;/em&gt; at the next scrape, &lt;em&gt;firing&lt;/em&gt; after the 30-second hold, Alertmanager delivered after its 10-second group wait, and the responder log printed &lt;code&gt;restarted container 'nginx'&lt;/code&gt;. Then - my favourite line ever - &lt;code&gt;docker ps&lt;/code&gt; showing nginx with an uptime of a few seconds while everything else showed twenty minutes.&lt;/p&gt;

&lt;p&gt;End to end, dead-to-healed took on my machine. Nobody touched anything.&lt;/p&gt;

&lt;p&gt;I ran it a few more times just to enjoy it, which promptly tripped my own circuit breaker. Satisfying to be refused by my own safety rail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The irony that wrote itself
&lt;/h2&gt;

&lt;p&gt;Here's the part I didn't plan. The same week I finished this write-up, I deployed a small side project - a cron expression converter - to a well-known static hosting platform. Single HTML file, simplest deployment imaginable.&lt;/p&gt;

&lt;p&gt;The deployment got stuck in "Queued". For twenty minutes. Then the retry got stuck too. It turned out to be a known platform-side queue jam that community threads report lasting anywhere from hours to over a day, and there was nothing I could do from my side but wait or fail over. I dragged the same file onto a different host and was live in ninety seconds.&lt;/p&gt;

&lt;p&gt;Which is the whole point of this article, really. Everything fails: my nginx, their deploy queue, your thing next Tuesday. The difference between a bad day and a non-event isn't preventing failure - it's how much of the response you've already built before it happens. My pipeline had a restart path ready; my deployment had a second host ready. Same principle, different layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd take this next
&lt;/h2&gt;

&lt;p&gt;If I were promoting this beyond the laptop, in rough order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication and idempotency&lt;/strong&gt; - Alertmanager retries webhooks; the responder should handle the same alert twice gracefully (it mostly does now, but "mostly" is doing heavy lifting).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation&lt;/strong&gt; - after the circuit breaker opens, page a human with the restart history attached, so they arrive with context instead of a cold start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail&lt;/strong&gt; - every automated action written somewhere durable. When automation acts on your behalf, "what did it do and when" must never be a mystery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smarter checks than up/down&lt;/strong&gt; - restart-on-death is the "hello world" of remediation. The interesting versions act on latency, error rates, and saturation before the service dies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are, of course, mature event-driven automation platforms that do all of this at enterprise scale with rulebooks and workflow engines. Having now built the primitive version by hand, I understand far better what those platforms are actually doing under the hood - which was the point of the weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The full repo - compose file, configs, responder, and a README with the exact break-it steps - is here: &lt;strong&gt;&lt;a href="https://github.com/suredares/Self-healing-Demo" rel="noopener noreferrer"&gt;https://github.com/suredares/Self-healing-Demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone it, run &lt;code&gt;docker compose up -d --build&lt;/code&gt;, then &lt;code&gt;docker stop nginx&lt;/code&gt; and watch your infrastructure quietly refuse to stay broken.&lt;/p&gt;

&lt;p&gt;If you build it and something behaves differently on your setup, tell me in the comments - genuinely curious what breaks on other machines. And if you've built auto-remediation that went wrong in an interesting way, I definitely want to hear that story.&lt;/p&gt;




&lt;p&gt;*I'm a DevOps engineer with 10 years across AWS, Azure, Kubernetes, Terraform and Ansible, writing about automation that actually works. I also built &lt;a href="https://cronport.netlify.app" rel="noopener noreferrer"&gt;CronPort&lt;/a&gt; - a free tool that converts cron expressions between crontab, Kubernetes, GitHub Actions, AWS EventBridge and Terraform.&lt;br&gt;
I also wrote up &lt;a href="https://dev.to/sureshbabunarayanan/five-terraform-state-mistakes-i-keep-seeing-after-13-years-and-the-fixes-4kc6"&gt;Five Terraform state mistakes I keep seeing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also sell a &lt;a href="https://sureshdevops.gumroad.com/l/terraform-aws-starter-kit" rel="noopener noreferrer"&gt;Terraform AWS Starter Kit&lt;/a&gt; if you want production-ready foundations without the setup day.&lt;/p&gt;

&lt;p&gt;If it saved you time, you can buy me a coffee &lt;a href="https://www.buymeacoffee.com/sureshnarayanan" rel="noopener noreferrer"&gt;Buymeacoffee&lt;/a&gt;*&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
