Fixing AWS EBS VolumeAttachment Stuck in 'Attaching' State: Multi-AZ Kubernetes Deadlock Resolved
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–30 mins depending on stale attachment cleanup
TL;DR
- What broke: Your EBS volume is AZ-locked to
us-east-1abut Kubernetes rescheduled the pod to a node inus-east-1b, or a staleVolumeAttachmentobject is blocking re-attachment after a node failure. - How to fix it: Force-delete the stale
VolumeAttachmentobject, verifyStorageClassusesvolumeBindingMode: WaitForFirstConsumer, and confirm the EBS CSI driver has correct IAM permissions. - Fast path: Use our Client-Side Sandbox below to paste your
VolumeAttachmentmanifest and StorageClass YAML — it auto-diagnoses the AZ mismatch and generates the corrected config without sending your data anywhere.
The Incident (What Does the Error Mean?)
Raw state from kubectl describe volumeattachment <name>:
Name: csi-abc123def456
Namespace:
Labels: <none>
Status:
Attached: false
Attach Error:
Message: context deadline exceeded
Spec:
Attacher: ebs.csi.aws.com
Node Name: ip-10-0-2-45.ec2.internal
Source:
Persistent Volume Name: pvc-7f3e1a2b
And from kubectl get events -n <namespace>:
Warning FailedAttachVolume pod/my-statefulset-0
Multi-Attach error for volume "pvc-7f3e1a2b":
Volume is already exclusively attached to one node
and can't be attached to another
Immediate consequence: The pod is stuck in ContainerCreating. The previous node (possibly terminated or drained) still holds the VolumeAttachment object in the API server. AWS EBS io1/gp3 volumes are ReadWriteOnce — they cannot attach to two nodes simultaneously, even across AZs. Kubernetes is waiting for the old attachment to release. It won't, because the node is gone.
The Attack Vector / Blast Radius
This is a StatefulSet availability bomb in disguise. Here's the cascade:
- Node failure or spot interruption in
us-east-1a— the node object is deleted but theVolumeAttachmentobject persists in etcd. - Kubernetes reschedules the pod to a node in
us-east-1b. The EBS CSI driver attempts to attach the volume. AWS rejects it — the volume is AZ-locked AND the old attachment record exists. - The
attach-detachcontroller has a default 6-minute timeout before it force-detaches. In production, this means 6+ minutes of guaranteed downtime per affected pod. - If
volumeBindingMode: Immediateis set on the StorageClass, the PVC was bound to an AZ at provision time with zero awareness of where pods will actually land. Every rescheduling event across AZs is a guaranteed attach failure. - For StatefulSets with multiple replicas, each replica hitting this on the same node failure event multiplies the outage window.
Secondary blast: if your node-problem-detector or cluster autoscaler is aggressively terminating nodes, you can enter a loop where volumes are perpetually in attaching state across a rolling set of nodes.
How to Fix It
Step 1: Identify the Stale VolumeAttachment
kubectl get volumeattachment
kubectl describe volumeattachment <attachment-name>
# Confirm the NodeName references a dead/missing node
kubectl get node <node-name>
# Expected: NotFound or NotReady
Step 2: Basic Fix — Force Delete the Stale VolumeAttachment
# Remove the finalizer first, otherwise delete hangs
kubectl patch volumeattachment <attachment-name> \
-p '{"metadata":{"finalizers":null}}' \
--type=merge
kubectl delete volumeattachment <attachment-name>
After deletion, the EBS CSI driver will re-trigger attachment to the current node. Verify the volume is not actually still attached in AWS before doing this — check the EC2 console or:
aws ec2 describe-volumes \
--volume-ids vol-0abc123def456 \
--query 'Volumes[*].Attachments'
If the AWS-side attachment still exists, force-detach from AWS first:
aws ec2 detach-volume --volume-id vol-0abc123def456 --force
Enterprise Best Practice — Fix the Root Cause in StorageClass
The real fix is preventing AZ-mismatched scheduling entirely.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3-production
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
kmsKeyId: "arn:aws:kms:us-east-1:123456789:key/your-key-id"
- volumeBindingMode: Immediate
+ volumeBindingMode: WaitForFirstConsumer
+ allowVolumeExpansion: true
reclaimPolicy: Retain
WaitForFirstConsumer delays PVC binding until a pod is scheduled, then provisions the EBS volume in the same AZ as the target node. This eliminates the AZ mismatch class of failures entirely.
Enterprise Best Practice — EBS CSI Driver IAM (Common Silent Failure)
If the CSI driver lacks permissions, the attachment silently times out with no useful error. Verify the node IAM role or IRSA role includes:
{
"Effect": "Allow",
"Action": [
"ec2:AttachVolume",
"ec2:DetachVolume",
"ec2:DescribeVolumes",
+ "ec2:DescribeVolumeStatus",
+ "ec2:DescribeInstances",
+ "ec2:ModifyVolume",
+ "ec2:DescribeAvailabilityZones"
],
"Resource": "*"
}
Missing DescribeVolumeStatus is a frequent cause of the CSI driver entering a silent retry loop that looks identical to an AZ mismatch.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Conftest / OPA Policy — Block Immediate Binding Mode
package kubernetes.storage
deny[msg] {
input.kind == "StorageClass"
input.provisioner == "ebs.csi.aws.com"
input.volumeBindingMode != "WaitForFirstConsumer"
msg := sprintf(
"StorageClass '%v' must use WaitForFirstConsumer for EBS in multi-AZ clusters",
[input.metadata.name]
)
}
Run in CI:
conftest test storageclass.yaml --policy policy/
2. Checkov — Terraform EBS Validation
checkov -d ./terraform --check CKV_AWS_189,CKV_AWS_3
# CKV_AWS_189: EBS encryption enabled
# CKV_AWS_3: EBS volume not publicly restorable
Add a custom Checkov check for WaitForFirstConsumer if managing StorageClass via Helm values in Terraform.
3. Alerting — Don't Rely on Manual Discovery
Add this Prometheus alert. Six minutes of silent attaching is too long to find manually:
- alert: EBSVolumeAttachmentStuck
expr: |
kube_volumeattachment_info{attacher="ebs.csi.aws.com"} == 1
and on(volumeattachment)
kube_volumeattachment_status_attached == 0
for: 3m
labels:
severity: critical
annotations:
summary: "EBS VolumeAttachment stuck for >3m: {{ $labels.volumeattachment }}"
runbook_url: "https://your-runbook/ebs-stuck-attachment"
4. Node Termination Handler
Deploy the AWS Node Termination Handler for spot instances. It cordons and drains nodes gracefully before termination, giving the attach-detach controller time to cleanly release VolumeAttachment objects before the node disappears.
helm install aws-node-termination-handler \
eks/aws-node-termination-handler \
--namespace kube-system \
--set enableSpotInterruptionDraining=true \
--set enableRebalanceMonitoring=true