<?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>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 13 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 13 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 13 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>
