<?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: Prateek Srivastava</title>
    <description>The latest articles on DEV Community by Prateek Srivastava (@prateek_srivastava_6a5661).</description>
    <link>https://dev.to/prateek_srivastava_6a5661</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%2F4086604%2F05a3d228-32cf-48b8-beb4-a0d1b624623b.jpg</url>
      <title>DEV Community: Prateek Srivastava</title>
      <link>https://dev.to/prateek_srivastava_6a5661</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prateek_srivastava_6a5661"/>
    <language>en</language>
    <item>
      <title>I Managed Kubernetes Across AWS, Azure, and GCP Simultaneously — Here's What Nobody Tells You</title>
      <dc:creator>Prateek Srivastava</dc:creator>
      <pubDate>Fri, 21 Aug 2026 19:11:46 +0000</pubDate>
      <link>https://dev.to/prateek_srivastava_6a5661/i-managed-kubernetes-across-aws-azure-and-gcp-simultaneously-heres-what-nobody-tells-you-2g69</link>
      <guid>https://dev.to/prateek_srivastava_6a5661/i-managed-kubernetes-across-aws-azure-and-gcp-simultaneously-heres-what-nobody-tells-you-2g69</guid>
      <description>&lt;p&gt;The day the same Helm chart behaved differently on three clouds in the same hour was the day I stopped trusting "cloud-agnostic" as a real thing.&lt;/p&gt;

&lt;p&gt;Nobody plans to manage Kubernetes on three clouds at the same time. It happens gradually — an acquisition brings an Azure footprint, a new business unit commits to GCP, and you already run EKS. Then one morning a page fires and you're staring at three terminal windows, three different cluster behaviours, one very angry incident bridge.&lt;/p&gt;

&lt;p&gt;I've spent the better part of two years in exactly that situation. What follows isn't a comparison chart you can find on any vendor blog. It's the stuff I learned the hard way — the silent differences that don't show up until something breaks in production at 2 AM.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📟 The Incident That Started It All&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We deployed the same Helm chart — identical values, same image tag — to EKS, AKS, and GKE within the same 20-minute release window. EKS came up healthy. AKS came up healthy. GKE's pods started, ran for about 90 seconds, then began OOMKilling in a loop. Same YAML. Same limits. Three different outcomes. Four hours to find why. That story is lesson #3.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Networking: The Biggest Lie — "Pods Are Just Pods"
&lt;/h2&gt;

&lt;p&gt;This is where most multi-cloud pain originates. Every Kubernetes tutorial tells you pods get their own IPs and can talk to each other. What it doesn't tell you is that &lt;em&gt;where those IPs come from&lt;/em&gt; completely changes your blast radius, subnet planning, security posture, and the failure modes you'll face at 3 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  EKS: Your pods eat your VPC subnet IPs
&lt;/h3&gt;

&lt;p&gt;EKS uses the AWS VPC CNI by default. Every pod gets a real, routable IP from your VPC subnet. This sounds fine until you have a &lt;code&gt;/24&lt;/code&gt; subnet and someone deploys a service with 200 replicas. The error isn't "out of IPs" — it's an ENI attachment timeout.&lt;/p&gt;

&lt;p&gt;The harder gotcha: each EC2 instance type has a max ENI count × max IPs per ENI cap. A &lt;code&gt;t3.medium&lt;/code&gt; can hold 3 ENIs × 6 IPs = 18 pod IPs max, minus 1 for the node. Pods sit &lt;strong&gt;Pending&lt;/strong&gt; on nodes with plenty of CPU and RAM — the constraint is invisible to the scheduler.&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;# Check ENI and IP capacity on a node&lt;/span&gt;
kubectl describe node &amp;lt;node-name&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"Allocatable"&lt;/span&gt;

&lt;span class="c"&gt;# Check aws-node daemonset for ENI attachment errors&lt;/span&gt;
kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;aws-node &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"err&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;exhaust"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Enable VPC CNI prefix delegation — one ENI prefix = 16 IPs instead of 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  AKS: Azure CNI pre-allocates IPs whether you like it or not
&lt;/h3&gt;

&lt;p&gt;AKS with Azure CNI reserves IPs per node based on &lt;code&gt;--max-pods&lt;/code&gt; (default 30) — before any pod runs. A 100-node cluster pre-consumes 3,000 VNet IPs. Use &lt;strong&gt;Azure CNI Overlay mode&lt;/strong&gt; if IP-constrained.&lt;/p&gt;

&lt;h3&gt;
  
  
  GKE: Clean until you need to peer networks
&lt;/h3&gt;

&lt;p&gt;GKE uses alias IP ranges, isolated from your main VPC. Clean story — until you peer your GKE VPC with on-prem and discover the pod CIDR overlaps your on-prem range, because you let GKE auto-assign ranges six months ago. &lt;strong&gt;Always explicitly define secondary ranges.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;EKS&lt;/th&gt;
&lt;th&gt;AKS&lt;/th&gt;
&lt;th&gt;GKE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod IP source&lt;/td&gt;
&lt;td&gt;VPC subnet (real IPs)&lt;/td&gt;
&lt;td&gt;VNet or overlay&lt;/td&gt;
&lt;td&gt;Alias IP ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP exhaustion risk&lt;/td&gt;
&lt;td&gt;High — ENI limits&lt;/td&gt;
&lt;td&gt;High — pre-allocated&lt;/td&gt;
&lt;td&gt;Low — isolated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common 3 AM failure&lt;/td&gt;
&lt;td&gt;ENI attachment timeout&lt;/td&gt;
&lt;td&gt;VNet IP exhaustion&lt;/td&gt;
&lt;td&gt;CIDR overlap during peering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. IAM + RBAC: Three Identity Models, One Misconfiguration Away from Disaster
&lt;/h2&gt;

&lt;h3&gt;
  
  
  EKS: The IRSA silent fallback trap
&lt;/h3&gt;

&lt;p&gt;If your IRSA annotation has a typo, the assume-role call silently fails and the pod falls through to the node's instance profile — often over-permissive. You won't see an error. Always verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I once spent three hours debugging why a pod had too much S3 access. A typo in the annotation. Silent fallback to the node's IAM role. The pod happily worked — with the wrong permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  AKS: Workload Identity federation — fragile on setup
&lt;/h3&gt;

&lt;p&gt;Three pieces must align exactly: Managed Identity, federated credential (exact OIDC issuer URL, including trailing slashes), and the Kubernetes service account annotation. One mismatch = silent 401.&lt;/p&gt;

&lt;h3&gt;
  
  
  GKE: Node rotation breaks implicit identity
&lt;/h3&gt;

&lt;p&gt;Enabling Workload Identity on a node pool removes the node's default Google service account. Any workload relying on implicit node identity breaks on the next node rotation. Audit before enabling.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Memory + cgroups: Why the Same Container OOMKilled on GKE but Not on EKS
&lt;/h2&gt;

&lt;p&gt;Here's the opening incident. Same image, same limits, EKS and AKS healthy, GKE OOMKilling every 90 seconds.&lt;/p&gt;

&lt;p&gt;GKE had moved to containerd with &lt;strong&gt;cgroup v2&lt;/strong&gt; on newer node images. EKS and AKS were still on cgroup v1.&lt;/p&gt;

&lt;p&gt;Our Java service used JVM ergonomics to auto-detect heap size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cgroup v1:&lt;/strong&gt; JVM reads from &lt;code&gt;/sys/fs/cgroup/memory/memory.limit_in_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroup v2:&lt;/strong&gt; JVM reads from &lt;code&gt;/sys/fs/cgroup/memory.max&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our old JVM didn't handle cgroup v2. It read &lt;strong&gt;host&lt;/strong&gt; memory (64GB) instead of the container limit (2GB), allocated an 8GB heap into a 2GB container, and OOMKilled within 90 seconds of starting.&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;# Check which cgroup version the container sees&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /proc/1/cgroup
&lt;span class="c"&gt;# cgroup v1: "12:memory:/kubepods/..."&lt;/span&gt;
&lt;span class="c"&gt;# cgroup v2: single line "0::/"&lt;/span&gt;

&lt;span class="c"&gt;# Check what heap the JVM actually allocated&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; java &lt;span class="nt"&gt;-XshowSettings&lt;/span&gt;:all &lt;span class="nt"&gt;-version&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; heap

&lt;span class="c"&gt;# See OOMKill events across all namespaces&lt;/span&gt;
kubectl get events &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OOMKilling &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.lastTimestamp'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; JDK 15+ (native cgroup v2 support) or JDK 11 with &lt;code&gt;-XX:+UseContainerSupport&lt;/code&gt;. Always explicitly set &lt;code&gt;-Xmx&lt;/code&gt; and &lt;code&gt;-Xms&lt;/code&gt;. This isn't just a Java problem — Go's &lt;code&gt;GOMAXPROCS&lt;/code&gt; and Python's &lt;code&gt;multiprocessing.cpu_count()&lt;/code&gt; have the same pattern.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;EKS&lt;/th&gt;
&lt;th&gt;AKS&lt;/th&gt;
&lt;th&gt;GKE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default node OS&lt;/td&gt;
&lt;td&gt;Amazon Linux 2023&lt;/td&gt;
&lt;td&gt;Ubuntu 22.04 / Azure Linux&lt;/td&gt;
&lt;td&gt;Container-Optimized OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cgroup version&lt;/td&gt;
&lt;td&gt;v2 (AL2023), v1 (AL2)&lt;/td&gt;
&lt;td&gt;v2 (Ubuntu 22.04+)&lt;/td&gt;
&lt;td&gt;v2 (COS since mid-2022)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. Storage: The AZ-Pinning Trap
&lt;/h2&gt;

&lt;p&gt;Block storage (EBS / Azure Disk / Persistent Disk) is AZ-specific on every cloud. Your PVC and the pod using it must be in the same AZ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scenario I've watched happen three times:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stateful pod uses a PVC in AZ-A. Traffic grows. Cluster Autoscaler spins up nodes in AZ-B and AZ-C. New pods stay &lt;strong&gt;Pending&lt;/strong&gt; — PVC is in AZ-A, the new nodes aren't. On-call engineer stares at CPU and memory graphs seeing nothing wrong.&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;# The event buried in pod describe that tips you off&lt;/span&gt;
kubectl describe pod &amp;lt;pending-pod&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"had volume node affinity conflict"&lt;/span&gt;
&lt;span class="c"&gt;# "0/12 nodes are available: 8 node(s) had volume node affinity conflict"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;volumeBindingMode: WaitForFirstConsumer&lt;/code&gt; on all storage classes. Delays PV creation until pod scheduling — volume always lands in the same AZ as the pod.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Load Balancers: Same Service Type, Three Different Outcomes
&lt;/h2&gt;

&lt;p&gt;Each cloud's controller uses completely different annotation namespaces for the same intent:&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;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# EKS — NLB instead of legacy Classic ELB&lt;/span&gt;
  &lt;span class="na"&gt;service.beta.kubernetes.io/aws-load-balancer-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;external"&lt;/span&gt;
  &lt;span class="na"&gt;service.beta.kubernetes.io/aws-load-balancer-nlb-target-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip"&lt;/span&gt;

  &lt;span class="c1"&gt;# AKS — internal (private) load balancer&lt;/span&gt;
  &lt;span class="na"&gt;service.beta.kubernetes.io/azure-load-balancer-internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;

  &lt;span class="c1"&gt;# GKE — completely different namespace&lt;/span&gt;
  &lt;span class="na"&gt;networking.gke.io/load-balancer-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ We once deployed a chart to GKE with only AWS annotations. GKE ignored them and created a &lt;strong&gt;public&lt;/strong&gt; Load Balancer. An internal API was internet-reachable for 40 minutes. SolarWinds monitoring caught it. It's the kind of miss that ends careers if the service is sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Validate LB visibility (internal vs public) on all three clouds in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Autoscaling: Cluster Autoscaler Is Not the Same Everywhere
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EKS scale-up path:&lt;/strong&gt; CA detects unschedulable pods → calls ASG → ASG launches EC2 → joins cluster → kubelet registers → pods schedule. That's &lt;strong&gt;4–8 minutes&lt;/strong&gt; minimum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GKE Node Auto Provisioning (NAP):&lt;/strong&gt; Managed by Google, reacts faster, can create new node pools automatically. Gotcha: NAP creates pools with labels and taints you didn't ask for. Pods without matching tolerations won't schedule — even though the cluster "scaled up" in your dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works across all three:&lt;/strong&gt; Layer KEDA (application-level scaling, seconds) under Cluster Autoscaler (node-level, minutes). Scale pods first; nodes are the last resort.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell Myself Before Starting All Over
&lt;/h2&gt;

&lt;p&gt;Multi-cloud Kubernetes isn't twice the work — it's about six times the work, because every subtle difference compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standardise observability first.&lt;/strong&gt; Before you standardise deployments, standardise how you look at your clusters. Same dashboards, same alert expressions, same log structure. When the incident fires at 2 AM, you want muscle memory, not translation overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document differences, not just similarities.&lt;/strong&gt; Your runbooks should say "on EKS, check X; on AKS, check Y; on GKE, check Z" — not pretend the clouds are the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Helm charts on all three before every major release.&lt;/strong&gt; 20 minutes of CI is cheaper than one 4-hour incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never trust "cloud-agnostic" on the label.&lt;/strong&gt; Especially networking, storage, and identity. Those break silently rather than loudly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The day you stop being surprised by cloud differences is the day you've actually become a multi-cloud SRE. Everything before that is just surviving the learning curve.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;If you're just starting this journey: keep a war journal. Every weird behaviour, every gotcha that cost you an hour — write it down. In six months it's worth more than any certification.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://prateeksrivastav598.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; for more incident stories from production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Our EKS Node Crashed and Four Auto-Recovery Mechanisms All Failed. Here's Why.</title>
      <dc:creator>Prateek Srivastava</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:56:58 +0000</pubDate>
      <link>https://dev.to/prateek_srivastava_6a5661/our-eks-node-crashed-and-four-auto-recovery-mechanisms-all-failed-heres-why-93l</link>
      <guid>https://dev.to/prateek_srivastava_6a5661/our-eks-node-crashed-and-four-auto-recovery-mechanisms-all-failed-heres-why-93l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ TL;DR: An EKS worker node running 30+ pods crashed at 08:43 IST on Aug 16. EC2 Auto Scaling didn't act. EKS Node Auto Repair was never enabled. Cluster Autoscaler had no headroom (Min=Max=6). Our memory alert fired 82 seconds after the node was already dead. We fixed it manually at 12:30 IST — 4 hours later. Every fix is one or two CLI commands. Here's the full breakdown.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When an EKS worker node goes down, Kubernetes is supposed to handle it. That's the whole pitch — self-healing infrastructure. Pods evict, workloads reschedule, the cluster heals itself.&lt;/p&gt;

&lt;p&gt;In a recent incident, a node running 30+ pods crashed at 08:43 IST. None of the four auto-recovery mechanisms we had in place did anything. The node sat NotReady for almost 4 hours. We discovered it 82 seconds after it died — from a memory alert that fired when the node was already gone — and fixed it manually at 12:30 IST.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't that the node crashed. It was that four different layers of protection failed for four completely different reasons.&lt;/p&gt;

&lt;p&gt;Here's exactly what failed and why.&lt;/p&gt;

&lt;p&gt;The Node That Looked Healthy Until It Didn't&lt;/p&gt;

&lt;p&gt;ip-10-&lt;em&gt;-&lt;/em&gt;-*.ec2.internal had been running since Jun 28. From Kubernetes' perspective, it was Ready. CPU requests at ~97%. Memory requests at ~69%. Nothing alarming.&lt;/p&gt;

&lt;p&gt;The actual picture was very different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allocated resources:
  Resource    Requests       Limits
  cpu         ~97%           ~545%
  memory      ~69%           ~358%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;358% memory limit overcommit. Kubernetes scheduled 37 pods there because requests showed 69% — but if those pods pushed toward their limits, the node had no chance. The requests were lies; the limits were the truth. And the truth said this node was 3.5x overloaded.&lt;/p&gt;

&lt;p&gt;Underneath the Ready status, the containerd runtime had been in a degraded state since Jul 20, when the OOM killer had hard-killed a MongoDB process. The kernel had logged two containerd-shim deadlocks (Jul 1 and Jul 29) that the node had somehow absorbed. Neither showed up anywhere in monitoring. The control plane saw nothing wrong.&lt;/p&gt;

&lt;p&gt;On Aug 16 at 08:22 IST, a service responsible for processing audit events started leaking memory. ELK showed node memory at 81.6% at 08:22:57 IST. By 08:43 IST the service had grown from 2.0GB to 6.2GB — a 4.2GB spike in 21 minutes — pushing the node from ~81% to ~96%.&lt;/p&gt;

&lt;p&gt;The memory pressure triggered a third containerd-shim deadlock on the already-corrupted runtime. This time it was fatal. At 08:43:22 IST, SSM lost contact. Kubelet posted its last heartbeat. At 08:45:10, the node went NotReady.&lt;/p&gt;




&lt;p&gt;Now watch how every auto-recovery mechanism missed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure #1 — EC2 Auto Scaling Health Check&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ASG protecting this nodegroup was configured with EC2-level health checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HealthCheckType: EC2
Min: 6 | Max: 6 | Desired: 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EC2 health checks have exactly one question: is the VM powered on?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instance-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;region&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'InstanceStatuses[].[InstanceState.Name,SystemStatus.Status,InstanceStatus.Status]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+------+------+
|  running|  ok  |  ok  |
+---------+------+------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance was running. System status: ok. Instance status: ok. The AWS hypervisor was satisfied. The deadlock inside containerd was completely invisible at the hypervisor layer — the kernel was alive, the VM was up, the NIC was responding to health probes.&lt;/p&gt;

&lt;p&gt;EC2 health check never triggered. No replacement instance was launched.&lt;/p&gt;

&lt;p&gt;The lesson: EC2 health checks only detect hardware failure or VM termination. They cannot detect OS-level hangs, kubelet death, containerd deadlocks, or any software failure that doesn't take down the underlying VM. For Kubernetes node health, EC2 checks are nearly useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure #2 — EKS Node Auto Repair&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EKS Node Auto Repair is designed exactly for this scenario. When a node stays NotReady for a defined period, it automatically cordons, drains, and replaces it. No manual intervention needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks describe-nodegroup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodegroup-name&lt;/span&gt; &amp;lt;nodegroup_name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'nodegroup.nodeRepairConfig'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;null. Never configured. Never enabled.&lt;/p&gt;

&lt;p&gt;This feature was available. We had just never set it up. The node sat NotReady for nearly 4 hours while the feature that would have replaced it in 10 minutes was turned off by default.&lt;/p&gt;

&lt;p&gt;The lesson: EKS Node Auto Repair is not enabled by default. Check every nodegroup right now with the command above. If you get null, you have this gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure #3 — Cluster Autoscaler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cluster autoscaler was running. It's supposed to handle exactly this kind of situation. But our configuration made it powerless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Min: 6 | Max: 6 | Desired: 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Min equals Max. The autoscaler has no headroom to operate. To replace a broken node, it needs to launch a new one first (going to 7, violating Max), drain the broken node, then terminate it (back to 6). With Max=6, it couldn't even start that sequence.&lt;/p&gt;

&lt;p&gt;The autoscaler logs confirmed it saw the problem — and couldn't act:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I0817 12:17:57 pre_filtering_processor.go:67]
Skipping ip-*-*-*-*.ec2.internal — node group min size reached
(current: 6, min: 6)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It knew the node was bad. It had no authority to fix it.&lt;/p&gt;

&lt;p&gt;There was a second problem: the cluster-autoscaler pod itself was running on the broken node, stuck in Terminating state alongside 25+ other pods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kube-system  cluster-autoscaler-5********8  1/1  Terminating  0  6d5h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing responsible for replacing the broken node was stuck on the broken node.&lt;/p&gt;

&lt;p&gt;The lesson: Min=Max is a zero-headroom configuration that paralyzes automated recovery. Set Max to at least Min+2 on every nodegroup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure #4 — Memory Alert (Fired Too Late to Matter)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We had a memory alert configured — running on an 11-minute cron interval, watching for node memory usage above 95%.&lt;/p&gt;

&lt;p&gt;ELK showed the node at 81.6% for hours before the incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IST timestamp    Node memory %
08:22:57         81.6%
08:33:57         81.6%
08:44:57         80.6%  ← node already down, stale value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 4.2GB memory spike happened entirely between two check cycles. At 08:22 the node was fine. At 08:44 the check returned stale data because the node was already gone.&lt;/p&gt;

&lt;p&gt;The alert fired at 08:44 IST. The node went unreachable at 08:43:22 IST. The alert was 82 seconds behind.&lt;/p&gt;

&lt;p&gt;This is a structural problem: the metric source is the node itself. When the node dies, metrics stop. If the crash happens between alert evaluation cycles, you miss it entirely. The faster a node crashes, the more likely it dies between evaluations.&lt;/p&gt;

&lt;p&gt;The lesson: Metric-based node alerts have a fundamental blind spot. If the node is the source of the metric, the metric disappears when the node dies.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What We're Fixing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable EKS Node Auto Repair (do this first)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks update-nodegroup-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodegroup-name&lt;/span&gt; &amp;lt;nodegroup_name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-repair-config&lt;/span&gt; &lt;span class="nv"&gt;enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a node stays NotReady for the configured threshold, EKS automatically cordons, drains, and replaces it. The control plane makes this decision — it doesn't depend on the broken node reporting its own failure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set Max &amp;gt; Min on Every ASG
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws autoscaling update-auto-scaling-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-scaling-group-name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-size&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-size&lt;/span&gt; 8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Max needs to be at least Min+2 to give the autoscaler room to launch a replacement before removing the broken instance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch to ELB Health Checks
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws autoscaling update-auto-scaling-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-scaling-group-name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-type&lt;/span&gt; ELB &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-grace-period&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ELB health checks evaluate actual application-layer response, not just hypervisor state.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace Metric-Based Node Alert with Condition-Based Alert
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This fires even when the node is completely dead.&lt;/span&gt;
&lt;span class="c1"&gt;# kube-state-metrics reads from the API server, not from the node itself.&lt;/span&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;NodeNotReady&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;kube_node_status_condition{condition="Ready",status="true"} == &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;90s&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;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node&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;$labels.node&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;NotReady&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;90s&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;EKS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Auto&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Repair&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;engage"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kube-state-metrics watches the Kubernetes API server — not the node. It fires even when the node is completely unreachable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforce Memory Overcommit Limits
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;yaml&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;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;LimitRange&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;memory-ratio-limit&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-namespace&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;limits&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;Container&lt;/span&gt;
    &lt;span class="na"&gt;maxLimitRequestRatio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4"&lt;/span&gt;   &lt;span class="c1"&gt;# limit cannot exceed 4x request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces the actual memory ceiling to stay within a predictable range of what the scheduler sees. A pod with a 1Gi request cannot have a 16Gi limit.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The Broader Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This wasn't an unusual incident. A node accumulated damage silently for 46 days. Nothing alerted. Nothing flagged the degraded containerd state. The node reported Ready to the control plane while internally the runtime was already compromised.&lt;/p&gt;

&lt;p&gt;When the fatal event came — a memory spike from an unrelated service — four recovery mechanisms failed in different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 health checks were asking the wrong question (is the VM up?)&lt;/li&gt;
&lt;li&gt;EKS Auto Repair was never turned on&lt;/li&gt;
&lt;li&gt;The autoscaler had no room to maneuver (Min=Max)&lt;/li&gt;
&lt;li&gt;The metric-based alert fired 82 seconds after the node was already dead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any one of these gaps would have slowed recovery. All four together meant 4 hours of a NotReady node and a manual reboot at noon.&lt;/p&gt;

&lt;p&gt;The fixes are each one or two commands. None of them are complicated. They just require knowing the gaps exist.&lt;/p&gt;

&lt;p&gt;Check Your Cluster Right Now&lt;/p&gt;

&lt;p&gt;Run this on every nodegroup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks describe-nodegroup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &amp;lt;your-cluster&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodegroup-name&lt;/span&gt; &amp;lt;your-nodegroup&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'nodegroup.nodeRepairConfig'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get null — you have this gap. Fix it before the next 2 AM alert.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The names, cluster/node identifiers, timestamps, instance IDs, pod counts, resource values, and other incident-specific details in this article have been intentionally changed or fictionalized for privacy and security. The failure pattern and technical lessons remain representative of the original incident.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://prateeksrivastav598.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; for more incident stories from production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>aws</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
