EFS CSI Driver

The EFS CSI driver mounts an Amazon EFS file system into pods over NFS. Reach for it when several pods, possibly on different nodes and in different zones, need to read and write the same files: shared uploads, CMS content, ML training data, build caches.

It is the opposite trade-off from EBS: you gain ReadWriteMany and multi-AZ access, and you pay with higher per-operation latency.

How it works

One EFS file system is shared by many PersistentVolumes. Isolation between them comes from EFS access points: each access point is a directory inside the file system with an enforced POSIX user, group and root path. With dynamic provisioning, every PVC gets its own access point.

EFS file system fs-0123 (regional, mount target in each AZ)
├── /dynamic/pvc-aaa   ← access point, uid 1001   → PVC "uploads"
├── /dynamic/pvc-bbb   ← access point, uid 1002   → PVC "reports"
└── /shared            ← static PV, mounted by several teams

Pods reach the file system through a mount target, an ENI in each Availability Zone. This is plain VPC networking, so two things must be true before anything mounts:

  1. A mount target exists in every zone where nodes run.
  2. The mount target’s security group allows inbound TCP 2049 from the node security group (or the pod security group, if you use security groups for pods).

A missing rule here is the most common EFS failure on EKS, and it shows up as a pod stuck in ContainerCreating with a mount timeout.

Install

aws eks create-addon \
  --cluster-name my-cluster \
  --addon-name aws-efs-csi-driver \
  --pod-identity-associations \
    serviceAccount=efs-csi-controller-sa,roleArn=arn:aws:iam::111122223333:role/efs-csi-controller

The role uses the managed policy AmazonEFSCSIDriverPolicy, which lets the controller create and delete access points. Static provisioning needs no AWS permissions at all, because nothing is created — the driver only mounts.

Dynamic provisioning

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: efs
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: fs-0123456789abcdef0
  directoryPerms: "700"
  basePath: /dynamic
  gidRangeStart: "1000"
  gidRangeEnd: "2000"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: uploads
spec:
  accessModes: [ReadWriteMany]
  storageClassName: efs
  resources:
    requests:
      storage: 5Gi # required by the API, ignored by EFS

The storage request is not a quota. EFS is elastic and the driver does not enforce the size, so a single noisy PVC can grow without limit and you are billed for what is stored.

Static provisioning

Use a hand-written PV when the file system already holds data or is shared across clusters:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: shared-assets
spec:
  capacity:
    storage: 5Gi
  accessModes: [ReadWriteMany]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-0123456789abcdef0::fsap-0aaaabbbbccccdddd

The volumeHandle format is fileSystemId:subPath:accessPointId; leave parts empty to mount the root.

Performance and cost

  • Latency. Every operation is a network round trip, typically low single-digit milliseconds. Databases, anything doing many small fsyncs, and tools that walk large directory trees (node_modules, Git checkouts) run badly on EFS.
  • Throughput mode. Elastic throughput scales automatically and bills per GB transferred; it suits spiky workloads. Provisioned throughput is cheaper when load is steady and predictable.
  • Storage classes. Lifecycle policies move cold files to Infrequent Access and Archive automatically. See EFS cost optimization.
  • Encryption. In transit is on by default through the driver’s TLS tunnel; at rest is a property of the file system and must be chosen at creation.

Limits

LimitDetail
Access points per file system1,000, which caps dynamically provisioned PVCs per file system
FargateStatic provisioning only; the driver is built into Fargate nodes
File locking and ownershipAn access point forces one uid/gid, so chown inside the container fails — set fsGroup to match instead
Windows nodesNot supported

Across the wiki