RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
AKS NodePressure Disk CSI Eviction Tool
02. Root Cause Vector

AKS node memory exhaustion triggers kubelet eviction of pods via NodePressure, compounded by Azure Disk CSI volume attachment overhead ca...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Performance

AKS NodePressure Disk CSI Eviction Tool

AKS node memory exhaustion triggers kubelet eviction of pods via NodePressure, compounded by Azure Disk CSI volume attachment overhead causing cascading workload failure.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(54L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: v1
kind: Pod
metadata:
  name: legacy-payment-processor-pod
  namespace: pci-secure-zone
  annotations:
    # TODO: This annotation is a hack to bypass the old scheduler, but it's causing issues with the new CSI driver
    aks.microsoft.com/scheduler: legacy-compat
    # VULNERABILITY: The memory request is set far below typical usage, causing the pod to be scheduled on a node with insufficient memory headroom, but the limit is high, leading to sudden memory pressure when the app spikes.
spec:
  containers:
  - name: payment-api
    image: my-registry.internal/legacy-api:v2.1.3-unstable
    # Workaround for the TLS cert issue in the old image
    command: ["/bin/sh"]
    args: ["-c", "while true; do node /app/server.js; sleep 5; done"]
    env:
    - name: AWS_ACCESS_KEY_ID
      value: "AKIAIOSFODNN7EXAMPLE"
    - name: AWS_SECRET_ACCESS_KEY
      value: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    - name: STRIPE_API_KEY
      value: "sk_live_4eC39HqLyjWDarjtT1zdp7dc"
    - name: DB_CONNECTION_STRING
      value: "postgresql://user:LegacyP@ssw0rd123!@pci-db.internal:5432/payments?sslmode=require"
    # The following RSA key is a dummy for testing SSH tunnel fallback (never used in prod, supposedly)
    - name: SSH_PRIVATE_KEY
      value: |
        -----BEGIN RSA PRIVATE KEY-----
        MIIEowIBAAKCAQEArGJ7vJ7H4QzK8kKj8pLm9qWtRzXwZbY8vY6pQ2nT0qZ
        ... [truncated for brevity] ...
        VpP7QKBgQCqGKp7v7J8kQ9qK8w8Kj8pLm9qWtRzXwZbY8vY6pQ2nT0qZ
        -----END RSA PRIVATE KEY-----
    resources:
      requests:
        memory: "64Mi"   # This was set low to fit on smaller nodes during the migration, but the app actually needs ~512Mi at peak
        cpu: "100m"
      limits:
        memory: "2Gi"    # The limit is high, allowing the pod to consume memory up to this point, but the node may not have enough free memory to accommodate this spike
        cpu: "1"
  nodeSelector:
    kubernetes.io/os: linux
    # This selector forces the pod onto a specific node pool that's already under memory pressure from other legacy pods
    agentpool: legacy-pool
  tolerations:
  - key: "legacy-system"
    operator: "Exists"
    effect: "NoSchedule"
  # Old volume mount for compatibility - might conflict with CSI driver eviction policies
  volumes:
  - name: payment-data
    persistentVolumeClaim:
      claimName: pci-payment-pvc  # This PVC uses the Azure Disk CSI driver, but the pod's memory behavior triggers eviction before disk pressure
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing AKS NodePressure Memory Eviction with Azure Disk CSI: Root Cause & Recovery Guide

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 15–45 mins depending on node pool size

TL;DR

  • What broke: Kubelet on an AKS node hit the hard memory eviction threshold (memory.available<100Mi by default), evicting your pod. Azure Disk CSI VolumeAttachment objects remained orphaned, blocking rescheduling on a new node.
  • How to fix it: Set explicit resources.requests and resources.limits on every container, tune kubelet eviction thresholds via a custom KubeletConfig, and patch the stuck VolumeAttachment if the CSI disk won't detach.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor your pod spec and node pool config without leaking your cluster credentials.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

You will see this in kubectl describe pod <pod-name>:

Status:     Failed
Reason:     Evicted
Message:    The node was low on resource: memory.
            Threshold quantity: 100Mi, available: 42Mi.
            Container <name> was using 1.9Gi, request is 0.

And on the node:

kubectl describe node <node-name>

Conditions:
  Type                 Status
  MemoryPressure       True     # <-- kubelet has declared war
  DiskPressure         False
  PIDPressure          False
  Ready                False

Immediate consequence: Kubelet terminates pods in priority order — BestEffort first (no requests/limits set), then Burstable, then Guaranteed. If your pod had no resources.requests, it is BestEffort and dies first, every time. The Azure Disk CSI driver then fails to detach the PersistentVolume cleanly because the pod was force-killed, leaving a VolumeAttachment object in Terminating state that blocks the pod from starting on a new node.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a single-pod problem. The cascade goes:

  1. One noisy-neighbor pod (no memory limit) balloons and consumes node memory.
  2. Kubelet evicts BestEffort pods — likely your stateful workloads with CSI disks, since those are often misconfigured.
  3. Azure Disk CSI VolumeAttachment gets stuck — the external-attacher sidecar in the CSI controller can't issue a ControllerUnpublishVolume to the Azure API fast enough before the node is marked NotReady.
  4. Pod reschedule is blocked — Kubernetes scheduler sees the PV still attached to the dead node. The pod sits in ContainerCreating or Pending indefinitely with Multi-Attach error for volume.
  5. If you have no PodDisruptionBudget, a rolling eviction can take down your entire Deployment simultaneously.
  6. Node pool autoscaler may spin up a new node, but the stuck VolumeAttachment prevents the PV from binding to it — so you now have a new node and still a broken pod.

Blast radius: Full service outage for any stateful workload (databases, message queues, file processors) running on the affected node pool.


How to Fix It

Step 1: Unblock the Stuck VolumeAttachment (Immediate)

If your pod is stuck in ContainerCreating on a new node:

# Identify the stuck attachment
kubectl get volumeattachment

# Force-delete the finalizer — do this ONLY if the source node is confirmed dead/drained
kubectl patch volumeattachment <va-name> \
  -p '{"metadata":{"finalizers":null}}' \
  --type=merge

Warning: Only remove the finalizer if you have confirmed the Azure Disk is not actually attached to any running VM. Check in the Azure Portal under the disk's "Managed by" field.

Step 2: Fix the Root Cause — Resource Requests & Limits

The BestEffort QoS class is the direct cause of priority eviction. Every container needs explicit requests.

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: my-stateful-app
 spec:
   template:
     spec:
       containers:
       - name: app
         image: myrepo/app:1.4.2
-        # No resources defined — BestEffort QoS, evicted first
+        resources:
+          requests:
+            memory: "512Mi"
+            cpu: "250m"
+          limits:
+            memory: "1Gi"
+            cpu: "1000m"

Enterprise Best Practice: Use Guaranteed QoS by setting requests == limits. This makes kubelet treat your pod as the last resort for eviction.

         resources:
           requests:
-            memory: "512Mi"
-            cpu: "250m"
+            memory: "1Gi"
+            cpu: "500m"
           limits:
             memory: "1Gi"
-            cpu: "1000m"
+            cpu: "500m"
+        # requests == limits => Guaranteed QoS class

Step 3: Tune Kubelet Eviction Thresholds via AKS KubeletConfig

The default memory.available<100Mi hard eviction threshold is dangerously low for nodes running CSI workloads. Raise the soft threshold so kubelet starts reclaiming memory gracefully before hitting the hard wall.

 apiVersion: v1
 kind: ConfigMap
 metadata:
   name: kubelet-config
 # Apply via AKS node pool KubeletConfig in the ARM template or Bicep:
---
 apiVersion: 2023-01-01
 type: Microsoft.ContainerService/managedClusters/agentPools
 properties:
   kubeletConfig:
-    # Default: no custom eviction thresholds
+    evictionSoft:
+      memory.available: "300Mi"
+      nodefs.available: "10%"
+    evictionSoftGracePeriod:
+      memory.available: "2m"
+      nodefs.available: "2m"
+    evictionHard:
+      memory.available: "150Mi"
+      nodefs.available: "5%"
+    evictionMaxPodGracePeriod: 90

Apply via Azure CLI:

az aks nodepool update \
  --resource-group <rg> \
  --cluster-name <cluster> \
  --name <nodepool> \
  --kubelet-config kubelet-config.json

Step 4: Add a PodDisruptionBudget

+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  name: my-stateful-app-pdb
+spec:
+  minAvailable: 1
+  selector:
+    matchLabels:
+      app: my-stateful-app

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

OPA/Gatekeeper Policy — Block BestEffort QoS at Admission

Deploy this ConstraintTemplate to reject any pod without resource requests:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: requireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: RequireResourceLimits
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package requireresourcelimits
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.requests.memory
          msg := sprintf("Container '%v' missing memory request. BestEffort QoS is prohibited.", [container.name])
        }

Checkov — Scan IaC Before Apply

# In your CI pipeline (GitHub Actions, Azure DevOps)
checkov -d ./k8s-manifests \
  --check CKV_K8S_11 \ # CPU limits
  --check CKV_K8S_13 \ # Memory limits
  --check CKV_K8S_10 \ # CPU requests
  --check CKV_K8S_12    # Memory requests

Node Pool Sizing — Right-Size Before You Tune Thresholds

# Check actual memory consumption vs allocatable
kubectl describe node <node> | grep -A5 "Allocated resources"

# If requests > 80% of allocatable memory, scale the node pool
az aks nodepool scale \
  --resource-group <rg> \
  --cluster-name <cluster> \
  --name <nodepool> \
  --node-count <n+2>

Rule of thumb: Keep total pod memory requests below 70% of node allocatable memory to leave headroom for kubelet, system daemons, and CSI driver sidecars (which themselves consume 50–150Mi per node).

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does the Azure Disk CSI VolumeAttachment get stuck after a NodePressure eviction?
The root cause is a race condition between kubelet's forced eviction and the CSI external-attacher's ControllerUnpublishVolume call. When memory pressure triggers a hard eviction, kubelet terminates the pod faster than the CSI driver can detach the disk via the Azure API. This leaves the VolumeAttachment object with a dangling finalizer, causing a logical attachment in Kubernetes even if Azure has physically detached it. The result is orphaned state and potential data plane disruption. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that patches the finalizer safely, avoiding manual intervention risks like accidental resource deletion or misconfiguration.
Q2.What is the difference between BestEffort, Burstable, and Guaranteed QoS in the context of kubelet eviction?
Kubelet evicts pods in a strict hierarchy: BestEffort (no requests/limits) first, then Burstable (requests ≠ limits), then Guaranteed (requests == limits). If your stateful pod with a CSI disk lacks resource requests, it's classified as BestEffort and becomes the primary target during NodePressure. Manually configuring QoS classes is error-prone and introduces security vulnerabilities—misconfigured limits can cause resource starvation or container escapes, increasing blast radius. Instead, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces optimal QoS settings without human error.
Q3.How do I prevent AKS node memory pressure from affecting other pods on the same node?
Effective prevention requires a multi-layered approach: Namespace LimitRanges to enforce defaults, node pool isolation via taints/tolerations, VPA for right-sizing, and custom kubelet eviction thresholds. However, implementing this manually across large mono-repos or at scale creates a compliance nightmare—inconsistent configurations lead to audit failures and operational drift. For enterprise-grade, automated PR drafting under Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem, CI/CD-integrated fixes that ensure policy adherence and eliminate manual oversight risks in production environments.