Install EKS EBS CSI Driver¶
Install the Amazon EBS Container Storage Interface (CSI) driver to enable Kubernetes workloads on EKS to dynamically provision and manage EBS volumes as PersistentVolumes. Follow the sequence to create the IAM role, install the addon, apply the fallback workaround when iam:PassRole is unavailable, and verify the installation.
Meet Prerequisites¶
Ensure the following prerequisites are in place before starting:
- An EKS cluster is running and
kubectlis configured. eksctlis installed.- AWS CLI is installed and authenticated.
- Access exists to create IAM roles and install EKS addons, or at minimum to create IAM roles and patch Kubernetes ServiceAccounts.
Note
Treat iam:PassRole as a separate and mandatory IAM permission for the standard addon installation path. Without it, the aws eks create-addon command fails even when the IAM role itself exists.
Export the Cluster Name¶
Export the cluster name as an environment variable:
Verify the variable before continuing:
Warning
Verify that $CLUSTER_NAME is not empty. If the variable is empty, AWS CLI returns the following error:
Create the IAM Role¶
Run the following command:
eksctl create iamserviceaccount \
--name ebs-csi-controller-sa \
--namespace kube-system \
--cluster $CLUSTER_NAME \
--role-name AmazonEKS_EBS_CSI_DriverRole \
--role-only \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
--approve
This command creates the IAM role required by the EBS CSI driver. It configures the role for IRSA (IAM Roles for Service Accounts) so the Kubernetes ServiceAccount can later assume the role through the cluster OIDC provider.
| Flag | Purpose |
|---|---|
--name ebs-csi-controller-sa | Define the Kubernetes ServiceAccount name that will later use this role. |
--namespace kube-system | Define the namespace where the controller runs. |
--cluster $CLUSTER_NAME | Bind the trust relationship to the target cluster OIDC provider. |
--role-name AmazonEKS_EBS_CSI_DriverRole | Assign a fixed IAM role name. |
--role-only | Create only the IAM role. Do not create or annotate the Kubernetes ServiceAccount. |
--attach-policy-arn ...AmazonEBSCSIDriverPolicy | Attach the AWS-managed policy required by the EBS CSI controller. |
--approve | Apply immediately without prompting for confirmation. |
Understand the role of --role-only¶
Use --role-only to stop eksctl from creating or modifying the Kubernetes ServiceAccount. Leave ServiceAccount creation to the EKS addon installation flow or to a manual patching step in the fallback path.
By default, eksctl create iamserviceaccount performs two actions:
- Create the IAM role.
- Create or update the Kubernetes ServiceAccount with the IAM role annotation.
With --role-only, perform only the first action.
Info
Do not expect ebs-csi-controller-sa to appear immediately after this step. This command creates the IAM role only. It does not create the Kubernetes ServiceAccount.
Check the addon versions¶
Check the supported addon versions for the cluster Kubernetes version:
Replace 1.35 with the actual cluster version if required.
Check the cluster version with:
Install the addon with the IAM role ARN¶
Retrieve the IAM role ARN:
ROLE_ARN=$(aws iam get-role --role-name AmazonEKS_EBS_CSI_DriverRole --query "Role.Arn" --output text)
Install the addon with the role ARN:
aws eks create-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--service-account-role-arn $ROLE_ARN
This command installs the AWS-managed EBS CSI addon on the cluster and attach the previously created IAM role to the addon-managed ServiceAccount.
| Flag | Purpose |
|---|---|
--cluster-name $CLUSTER_NAME | Select the target EKS cluster. |
--addon-name aws-ebs-csi-driver | Select the AWS-managed EBS CSI addon. |
--service-account-role-arn | Pass the IAM role ARN created earlier. |
--configuration-values '{"defaultStorageClass":{"enabled":true}}' | Enable creation of a default EBS-backed StorageClass. |
After successful completion, expect the following resources to appear:
- The EBS CSI controller deployment in
kube-system. - The
ebs-csi-controller-saServiceAccount inkube-system. - A default EBS-backed StorageClass.
Note
Use this path only when the IAM caller has iam:PassRole permission on AmazonEKS_EBS_CSI_DriverRole.
Apply the fallback workaround when iam:PassRole is unavailable¶
Use the fallback path when the environment does not allow adding the missing IAM permission, such as a third-party AWS playground or KodeKloud AWS playground. In that case, create the addon without passing the role ARN, then manually annotate the ServiceAccount and restart the controller.
Recognize the failure¶
Expect the standard addon command to fail with an error similar to the following:
AccessDeniedException: User ... is not authorized to perform: iam:PassRole on resource ... because no identity-based policy allows the iam:PassRole action
Create the addon without the role ARN¶
Run the addon installation without --service-account-role-arn:
aws eks create-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--resolve-conflicts "OVERWRITE"
Wait for the addon resources to appear.
Annotate the ServiceAccount manually¶
Retrieve the IAM role ARN:
ROLE_ARN=$(aws iam get-role --role-name AmazonEKS_EBS_CSI_DriverRole --query "Role.Arn" --output text)
Annotate the ServiceAccount manually:
kubectl annotate serviceaccount ebs-csi-controller-sa \
-n kube-system \
eks.amazonaws.com/role-arn=$ROLE_ARN \
--overwrite
Restart the controller¶
Restart the controller deployment so the pods pick up the new annotation:
Wait for the rollout to complete:
Warning
Use this workaround only when iam:PassRole cannot be granted. Prefer the standard addon installation path whenever the required IAM permission is available.
Verify the installation¶
Check the addon state:
aws eks describe-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--query "addon.status"
Check the deployment and ServiceAccount:
kubectl get deploy -n kube-system | grep ebs-csi
kubectl get sa -n kube-system | grep ebs-csi-controller-sa
Check the StorageClass:
Confirm the ServiceAccount annotation:
Configure the Default StorageClass (gp2 → gp3)¶
After installing the EBS CSI driver, running kubectl get sc shows a gp2 StorageClass that was never explicitly created:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
gp2 kubernetes.io/aws-ebs Delete WaitForFirstConsumer false 68m
This StorageClass is created automatically by EKS during cluster bootstrapping. It uses the in-tree EBS provisioner (kubernetes.io/aws-ebs), which is built into the Kubernetes node itself and does not require any CSI driver. AWS shipped this as the cluster default up through EKS 1.29.
This provisioner is deprecated. It does not support gp3 volume types. To use gp3, create a new StorageClass backed by the modern CSI provisioner (ebs.csi.aws.com) and demote gp2 from the default.
gp2 vs gp3¶
| Feature | gp2 | gp3 |
|---|---|---|
| Provisioner | kubernetes.io/aws-ebs (in-tree, deprecated) | ebs.csi.aws.com (CSI, current standard) |
| Price | $0.10/GiB-month | $0.08/GiB-month (20% cheaper) |
| Baseline IOPS | 3 IOPS/GiB, minimum 100 (size-dependent) | 3,000 flat regardless of volume size |
| Max IOPS | 16,000 | 16,000 |
| Max throughput | 250 MiB/s | 1,000 MiB/s |
| IOPS tuning | Tied to volume size | Independent of size |
allowVolumeExpansion | false (default) | true (configurable) |
The critical gp2 trap: a 10 GiB gp2 volume gets only 30 IOPS. A 10 GiB gp3 volume gets 3,000 IOPS at no extra cost.
Create the gp3 StorageClass¶
Apply the following manifest to create a gp3 StorageClass using the CSI provisioner:
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
EOF
| Field | Value | Reason |
|---|---|---|
provisioner | ebs.csi.aws.com | Uses the modern CSI driver, not the deprecated in-tree driver. |
parameters.type | gp3 | Selects the gp3 EBS volume type. |
volumeBindingMode | WaitForFirstConsumer | Delays volume creation until a pod is scheduled, ensuring the EBS volume is created in the same AZ as the pod. |
allowVolumeExpansion | true | Permits online volume resize without recreating the StorageClass later. |
is-default-class | "true" | Makes gp3 the default for any PVC that does not specify a StorageClass. |
Remove the default annotation from gp2¶
kubectl patch storageclass gp2 -p \
'{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "false"}}}'
Verify¶
Expect the following output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
gp2 kubernetes.io/aws-ebs Delete WaitForFirstConsumer false 80m
gp3 (default) ebs.csi.aws.com Delete WaitForFirstConsumer true 36s
Note
Leave gp2 in place. Deleting it can break workloads that reference storageClassName: gp2 explicitly. Only remove it after confirming no PVCs depend on it.
Any new PVC that does not specify a storageClassName will now automatically provision a gp3 EBS volume with 3,000 IOPS baseline.
Troubleshoot common failures¶
Fix iam:PassRole failures¶
Use the fallback workaround section in environments where IAM policy changes are not possible. If IAM policy changes are possible, add iam:PassRole permission on AmazonEKS_EBS_CSI_DriverRole and use the standard addon installation path.
Handle a missing ServiceAccount before addon installation¶
Treat this as expected behavior. The ServiceAccount is not created by the eksctl create iamserviceaccount ... --role-only command. The ServiceAccount appears only after addon installation, or after a manual creation path outside this runbook.
Quick Run¶
eksctl create iamserviceaccount \
--name ebs-csi-controller-sa \
--namespace kube-system \
--cluster $CLUSTER_NAME \
--role-name AmazonEKS_EBS_CSI_DriverRole \
--role-only \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
--approve
ROLE_ARN=$(aws iam get-role --role-name AmazonEKS_EBS_CSI_DriverRole --query "Role.Arn" --output text)
aws eks create-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--service-account-role-arn $ROLE_ARN
aws eks describe-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--query "addon.status"
sleep 15
kubectl get deploy -n kube-system | grep ebs-csi
kubectl get sa -n kube-system | grep ebs-csi-controller-sa
kubectl get storageclass
# Migrate default StorageClass from gp2 to gp3
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
EOF
kubectl patch storageclass gp2 -p \
'{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "false"}}}'
kubectl get sc
Fallback for iam:PassRole failures¶
aws eks create-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--resolve-conflicts "OVERWRITE"
sleep 15
kubectl annotate serviceaccount ebs-csi-controller-sa \
-n kube-system \
eks.amazonaws.com/role-arn=$ROLE_ARN \
--overwrite
kubectl rollout restart deployment ebs-csi-controller -n kube-system
kubectl rollout status deployment ebs-csi-controller -n kube-system
sleep 15
aws eks describe-addon \
--cluster-name $CLUSTER_NAME \
--addon-name aws-ebs-csi-driver \
--query "addon.status"
kubectl get deploy -n kube-system | grep ebs-csi
kubectl get sa -n kube-system | grep ebs-csi-controller-sa
kubectl get storageclass
kubectl get sa ebs-csi-controller-sa -n kube-system -o yaml | grep role-arn