Last modified October 15, 2026
Shared volumes with Amazon EFS
The default gp3 storage class in AWS clusters gives you EBS volumes, which a single node can mount at a time (ReadWriteOnce). When several pods on different nodes need the same files, a common option is to use Amazon Elastic File System (EFS) instead. EFS volumes support the ReadWriteMany access mode.
In this guide, you’ll:
- Install the AWS EFS CSI driver in your workload cluster.
- Create an EFS file system and its network plumbing with Crossplane.
Requirements
- A workload cluster on AWS (CAPA). Run all management cluster commands in the cluster’s organization namespace,
org-<ORGANIZATION>. - The
kubectl gsplugin, v6 or newer. kubectlaccess to the management cluster and to the workload cluster.- Permission to create Crossplane AWS resources on the management cluster. If you lack it, ask your Giant Swarm account engineer.
The examples use these placeholders:
<CLUSTER_NAME>: the name of your workload cluster.<ORGANIZATION>: the name of your organization.<REGION>: the AWS region of your workload cluster, for exampleeu-central-1.
Install the EFS CSI driver
The aws-efs-csi-driver-bundle chart runs on the management cluster. It creates the IAM role the driver needs, using Crossplane, and deploys the driver to your workload cluster.
You install the bundle with kubectl gs deploy chart. The command creates the Flux resources that install the chart: an OCIRepository pointing to the chart in the Giant Swarm registry, and a HelmRelease that installs it.
Deploy the bundle from the management cluster:
kubectl gs deploy chart \
--chart-name aws-efs-csi-driver-bundle \
--version 4.0.0 \
--organization <ORGANIZATION> \
--target-cluster <CLUSTER_NAME> \
--bundle
Check the repository releases for the latest version, and set it in --version. With --bundle, the HelmRelease runs on the management cluster in the organization namespace, using the automation service account. The platform injects the workload cluster ID into the bundle values, so you don’t need to pass any values.
To review the resources before you create them, add --dry-run. The command then prints them instead of applying them:
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
labels:
giantswarm.io/cluster: <CLUSTER_NAME>
name: <CLUSTER_NAME>-aws-efs-csi-driver-bundle
namespace: org-<ORGANIZATION>
spec:
interval: 10m0s
provider: generic
ref:
tag: 4.0.0
url: oci://gsoci.azurecr.io/charts/giantswarm/aws-efs-csi-driver-bundle
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
labels:
giantswarm.io/cluster: <CLUSTER_NAME>
name: <CLUSTER_NAME>-aws-efs-csi-driver-bundle
namespace: org-<ORGANIZATION>
spec:
chartRef:
kind: OCIRepository
name: <CLUSTER_NAME>-aws-efs-csi-driver-bundle
interval: 10m0s
releaseName: <CLUSTER_NAME>-aws-efs-csi-driver-bundle
serviceAccountName: automation
targetNamespace: org-<ORGANIZATION>
If you manage the management cluster resources with GitOps, commit this output to your repository instead of running the command without --dry-run.
The bundle creates a second HelmRelease, <CLUSTER_NAME>-aws-efs-csi-driver, which installs the driver itself and a Crossplane AWS IAM Role resource. Wait until both report Ready:
kubectl -n org-<ORGANIZATION> get helmrelease \
<CLUSTER_NAME>-aws-efs-csi-driver-bundle \
<CLUSTER_NAME>-aws-efs-csi-driver
After a few minutes, the driver runs in the kube-system namespace of your workload cluster. Check that the controller and the node pods are ready:
kubectl -n kube-system get deployment efs-csi-controller
kubectl -n kube-system get daemonset efs-csi-node
Create the EFS file system with Crossplane
An EFS file system needs three things before nodes can mount it:
- A security group that allows NFS traffic (TCP port 2049) from inside the VPC.
- The file system itself.
- One mount target per availability zone, placed in the cluster’s private subnets.
Every workload cluster comes with a Crossplane ProviderConfig named after the cluster. It grants Crossplane access to the cluster’s AWS account, so you reference it as <CLUSTER_NAME> in all resources below.
Look up the network details
Read the VPC ID, the VPC CIDR, and the private subnets from the cluster’s AWSCluster resource on the management cluster:
kubectl -n org-<ORGANIZATION> get awscluster <CLUSTER_NAME> \
-o jsonpath='{.spec.network.vpc.id} {.spec.network.vpc.cidrBlock}{"\n"}'
kubectl -n org-<ORGANIZATION> get awscluster <CLUSTER_NAME> \
-o jsonpath='{range .spec.network.subnets[?(@.isPublic==false)]}'\
'{.resourceID} {.availabilityZone}{"\n"}{end}'
The second command prints one private subnet per line, together with its availability zone. Pick one subnet per availability zone.
Create the security group and the file system
Create a file efs-filesystem.yaml. Replace <VPC_ID> with the VPC ID from the previous step:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
metadata:
name: <CLUSTER_NAME>-efs-sg
spec:
forProvider:
region: <REGION>
vpcId: <VPC_ID>
description: Allow NFS access to EFS
tags:
Name: <CLUSTER_NAME>-efs-sg
providerConfigRef:
name: <CLUSTER_NAME>
---
apiVersion: efs.aws.upbound.io/v1beta1
kind: FileSystem
metadata:
name: <CLUSTER_NAME>-efs
spec:
forProvider:
region: <REGION>
performanceMode: generalPurpose
encrypted: true
tags:
Name: <CLUSTER_NAME>-efs
providerConfigRef:
name: <CLUSTER_NAME>
Apply it to the management cluster and wait until both resources are ready:
kubectl apply -f efs-filesystem.yaml
kubectl wait --for=condition=Ready --timeout=5m \
securitygroup.ec2.aws.upbound.io/<CLUSTER_NAME>-efs-sg \
filesystem.efs.aws.upbound.io/<CLUSTER_NAME>-efs
Then read the AWS IDs that Crossplane got back:
kubectl get securitygroup.ec2.aws.upbound.io <CLUSTER_NAME>-efs-sg \
-o jsonpath='{.status.atProvider.id}{"\n"}'
kubectl get filesystem.efs.aws.upbound.io <CLUSTER_NAME>-efs \
-o jsonpath='{.status.atProvider.id}{"\n"}'
The first ID starts with sg-, the second one with fs-. You need both in the next step.
Open the NFS port and create the mount targets
Create a file efs-network.yaml. Replace <SECURITY_GROUP_ID>, <FILE_SYSTEM_ID>, and <VPC_CIDR> with your values, and add one MountTarget per private subnet:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupRule
metadata:
name: <CLUSTER_NAME>-efs-nfs
spec:
forProvider:
region: <REGION>
securityGroupId: <SECURITY_GROUP_ID>
type: ingress
protocol: tcp
fromPort: 2049
toPort: 2049
cidrBlocks:
- <VPC_CIDR>
providerConfigRef:
name: <CLUSTER_NAME>
---
apiVersion: efs.aws.upbound.io/v1beta1
kind: MountTarget
metadata:
name: <CLUSTER_NAME>-efs-<AVAILABILITY_ZONE>
spec:
forProvider:
region: <REGION>
fileSystemId: <FILE_SYSTEM_ID>
subnetId: <PRIVATE_SUBNET_ID>
securityGroups:
- <SECURITY_GROUP_ID>
providerConfigRef:
name: <CLUSTER_NAME>
# Repeat the MountTarget for each availability zone.
Apply it to the management cluster and wait until all mount targets are ready. This usually takes one to two minutes:
kubectl apply -f efs-network.yaml
kubectl get mounttarget.efs.aws.upbound.io
Give the mount targets a few minutes after they turn ready. Until AWS publishes the file system’s DNS name in the VPC, mounts fail with Failed to resolve "fs-....efs.<REGION>.amazonaws.com". Kubernetes retries the mount on its own, so pods start as soon as the name resolves.
Mount the volume in your pods
Run the following steps against the workload cluster.
Create a storage class
The storage class tells the driver which file system to use. With provisioningMode: efs-ap, the driver creates an EFS access point for each persistent volume claim.
In AWS, the root directory of an access point is optional and defaults to the root of the file system (/). The driver doesn’t rely on that default. It always sets the root directory of each access point to a new directory named after the persistent volume, with a unique ID appended. As a result, every claim gets its own directory on the shared file system and can’t see the data of other claims. To change this layout, use the basePath, subPathPattern, and ensureUniqueDirectory parameters of the storage class, described in the driver documentation.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: <FILE_SYSTEM_ID>
directoryPerms: "700"
uid: "1000"
gid: "1000"
The uid and gid parameters set the owner of the access point directory. Match them to the user your pods run as, so that non-root containers can write to the volume.
Create a persistent volume claim
Request a volume with the ReadWriteMany access mode:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-data
spec:
accessModes:
- ReadWriteMany
storageClassName: efs
resources:
requests:
storage: 5Gi
EFS grows and shrinks with the data you store, so the requested size isn’t enforced. Kubernetes still requires a value.
The claim turns Bound within seconds:
kubectl get pvc shared-data
Use the volume
Mount the claim like any other volume. Because the access mode is ReadWriteMany, all replicas of this deployment share the same files, no matter which node they run on:
apiVersion: apps/v1
kind: Deployment
metadata:
name: shared-data-demo
spec:
replicas: 2
selector:
matchLabels:
app: shared-data-demo
template:
metadata:
labels:
app: shared-data-demo
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- echo "hello from $(hostname)" >> /data/log.txt && sleep 3600
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: shared-data
To check that the replicas share the volume, read the file from one of the pods. It contains a line from each replica:
kubectl exec deploy/shared-data-demo -- cat /data/log.txt
Clean up
Delete the resources in reverse order, or the AWS resources get stuck on their dependencies:
In the workload cluster, delete the workloads, the persistent volume claims, and the storage class. With the default
reclaimPolicy: Delete, the driver removes the access points, including their data.On the management cluster, delete the mount targets and the security group rule:
kubectl delete -f efs-network.yamlOn the management cluster, delete the file system and the security group:
kubectl delete -f efs-filesystem.yamlTo remove the driver too, delete the bundle:
kubectl -n org-<ORGANIZATION> delete helmrelease,ocirepository \ <CLUSTER_NAME>-aws-efs-csi-driver-bundle
Further reading
- Persistent volumes
- AWS EFS CSI driver repository
- EFS CSI driver upstream documentation
- Crossplane AWS provider for EFS
Need help, got feedback?
We listen to your Slack support channel. You can also reach us at support@giantswarm.io. And of course, we welcome your pull requests!