← Back to list

Persistent Storage for ECS Fargate: Why Amazon EFS is the Cross-AZ Answer

How elastic file storage solves the hardest stateful container problem — tasks that live anywhere, data that lives everywhere.

Osman ALP in AWS Tip · 2026-03-31 11:57 · 51 claps · 9.1 min read paywalled
#aws #devops #storage #ec #ef
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval ☁️ · DevOps & Cloud

Persistent Storage for ECS Fargate: Why Amazon EFS is the Cross-AZ Answer

How elastic file storage solves the hardest stateful container problem — tasks that live anywhere, data that lives everywhere.

Containers are designed to be ephemeral. Spin one up, run a workload, tear it down — that’s the deal. But real applications are messy: they write logs, cache state, share configuration files, and generate data that outlives any single container lifecycle. When you add AWS Fargate’s serverless scheduling model — tasks placed across Availability Zones with no guarantee of landing on the same host twice — the problem of persistent, shared storage becomes your first architectural challenge.

This article dismantles that challenge. We’ll examine why Amazon EFS is the canonical answer for cross-AZ persistent volumes in Fargate workloads, walk through both the theory and the working code to wire it up, and surface the pitfalls that catch engineers in production.

The Cross-AZ Storage Problem

When ECS deploys Fargate tasks under a service with capacityProviderStrategy, it distributes tasks across multiple Availability Zones for resilience. Task A might land in us-east-1a, Task B in us-east-1b, and Task C in us-east-1c. If a task writes data to its local container filesystem, that data is isolated — other tasks can't see it, and when the task stops, the data disappears entirely.

The storage options available for Fargate reveal starkly different tradeoffs. Local bind mounts exist only for the lifetime of a single task. Amazon S3 is object storage with eventual consistency (and no POSIX semantics). Amazon EBS volumes are block-level and single-AZ-attached — you can’t mount the same EBS volume in two Availability Zones simultaneously. That leaves one native, purpose-built solution: Amazon Elastic File System (EFS).

Each Fargate task connects to the EFS file system through an EFS Mount Target deployed in its local subnet. A mount target is simply an elastic network interface inside your VPC that routes NFS traffic (port 2049) from the container to the EFS service. Because EFS replicates data across all AZs within a region, every task sees the same file system — simultaneously, with low latency.

Why EFS, and Not the Alternatives

The AWS storage landscape offers several options for containers, but each carries fundamental constraints when applied to Fargate’s multi-AZ topology. The table below anchors the comparison to the specific requirements: persistence across task restarts, concurrent access from multiple tasks, and Availability Zone portability.

  • EBS Multi-Attach (io1/io2 volumes) allows multiple EC2 instances to attach the same volume within a single AZ, but this is restricted to EC2 and requires application-level coordination to prevent data corruption. It is not available on Fargate.

Common Misconception: Amazon S3 is not a filesystem. It provides object storage via an HTTP API. Applications expecting POSIX semantics — file locking, directory operations, mmap(), append-in-place writes — will fail silently or throw exceptions against S3. EFS presents a true NFSv4.1 interface, which is what containerized applications typically expect.

Wiring It Up: Step-by-Step Implementation

Connecting an ECS Fargate task to EFS requires three coordinated pieces: the EFS file system with its mount targets, the security group rules allowing NFS traffic, and the task definition referencing the volume. Let’s build each layer.

Step 1: Create the EFS File System

EFS supports two throughput modes (bursting and provisioned) and two performance modes (generalPurpose and maxIO). For most Fargate workloads, generalPurpose with bursting throughput is the right starting point.

# Create the EFS file system with encryption at rest enabled
aws efs create-file-system \
  --performance-mode generalPurpose \
  --throughput-mode bursting \
  --encrypted \
  --tags Key=Name,Value=fargate-shared-fs \
  --query 'FileSystemId' \
  --output text

# Create one mount target per AZ (repeat for each subnet)
aws efs create-mount-target \
  --file-system-id fs-XXXXXXXX \
  --subnet-id subnet-AZ-A \
  --security-groups sg-efs-mt

aws efs create-mount-target \
  --file-system-id fs-XXXXXXXX \
  --subnet-id subnet-AZ-B \
  --security-groups sg-efs-mt

Step 2: Security Group Rules

NFS traffic flows over TCP port 2049. The EFS mount target security group must allow inbound port 2049 from the Fargate task security group. The Fargate task security group needs outbound port 2049 to the mount target security group. Nothing else is needed — the principle of least privilege applies here.

# Allow Fargate tasks to reach EFS mount targets on port 2049
aws ec2 authorize-security-group-ingress \
  --group-id sg-efs-mt \
  --protocol tcp \
  --port 2049 \
  --source-group sg-fargate-tasks

# Allow Fargate tasks to connect outbound to EFS
aws ec2 authorize-security-group-egress \
  --group-id sg-fargate-tasks \
  --protocol tcp \
  --port 2049 \
  --destination-group sg-efs-mt

Step 3: Task Definition with EFS Volume

The task definition is where the magic is declared. You register an efsVolumeConfiguration under the top-level volumes key, then reference it from each container via a mountPoints entry. The container process sees a normal directory — it has no idea it's talking to a network filesystem.

{
  "family": "fargate-efs-demo",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",

  // 1 ─ Declare the EFS volume at the task level
  "volumes": [
    {
      "name": "shared-data",
      "efsVolumeConfiguration": {
        "fileSystemId": "fs-XXXXXXXX",
        "rootDirectory": "/",
        "transitEncryption": "ENABLED",        // TLS in-flight
        "authorizationConfig": {
          "accessPointId": "fsap-XXXXXXXX",  // optional but recommended
          "iam": "ENABLED"                  // use task IAM role
        }
      }
    }
  ],

  "containerDefinitions": [
    {
      "name": "app",
      "image": "your-ecr-image:latest",

      // 2 ─ Mount the volume into the container at a specific path
      "mountPoints": [
        {
          "sourceVolume": "shared-data",
          "containerPath": "/mnt/shared",
          "readOnly": false
        }
      ]
    }
  ]
}

Step 4: Programmatic Access via boto3

If you’re provisioning infrastructure from Python — for example, in a deployment pipeline or a Lambda-driven automation — boto3 covers the full lifecycle from file system creation to task definition registration.

import boto3

ecs = boto3.client('ecs', region_name='us-east-1')

response = ecs.register_task_definition(
    family='fargate-efs-demo',
    networkMode='awsvpc',
    requiresCompatibilities=['FARGATE'],
    cpu='512',
    memory='1024',
    executionRoleArn='arn:aws:iam::123456789012:role/ecsTaskExecutionRole',
    taskRoleArn='arn:aws:iam::123456789012:role/ecsTaskRole',

    # ─ Volume declaration
    volumes=[
        {
            'name': 'shared-data',
            'efsVolumeConfiguration': {
                'fileSystemId': 'fs-XXXXXXXX',
                'rootDirectory': '/',
                'transitEncryption': 'ENABLED',
                'authorizationConfig': {
                    'accessPointId': 'fsap-XXXXXXXX',
                    'iam': 'ENABLED',
                },
            },
        }
    ],

    containerDefinitions=[
        {
            'name': 'app',
            'image': '123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest',
            'mountPoints': [
                {
                    'sourceVolume': 'shared-data',
                    'containerPath': '/mnt/shared',
                    'readOnly': False,
                }
            ],
            'logConfiguration': {
                'logDriver': 'awslogs',
                'options': {
                    'awslogs-group': '/ecs/fargate-efs-demo',
                    'awslogs-region': 'us-east-1',
                    'awslogs-stream-prefix': 'app',
                },
            },
        }
    ],
)

td_arn = response['taskDefinition']['taskDefinitionArn']
print(f'Registered: {td_arn}')

Step 5: Access Points for Multi-Tenant Isolation

When multiple services share the same EFS file system, EFS Access Points enforce a POSIX identity and a rooted directory path per application. Access Point fsap-001 might root to /service-a and run as UID 1000; fsap-002 roots to /service-b and runs as UID 2000. Tasks see only their own directory subtree — they cannot traverse up to the filesystem root.

aws efs create-access-point \
  --file-system-id fs-XXXXXXXX \
  --posix-user Uid=1000,Gid=1000 \
  --root-directory "Path=/service-a,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=755}" \
  --tags Key=Service,Value=service-a

Infrastructure as Code: CloudFormation & CDK

Production deployments should never be assembled by hand. The following CloudFormation snippet declares the EFS file system, a mount target in a given subnet, and a basic security group — ready to be embedded in a larger stack or called via Fn::ImportValue.

EFSFileSystem:
  Type: AWS::EFS::FileSystem
  Properties:
    Encrypted: true
    PerformanceMode: generalPurpose
    ThroughputMode: bursting
    FileSystemTags:
      - Key: Name
        Value: fargate-shared-fs

EFSMountTargetAZA:
  Type: AWS::EFS::MountTarget
  Properties:
    FileSystemId: !Ref EFSFileSystem
    SubnetId: !Ref PrivateSubnetAZA
    SecurityGroups: [!Ref EFSSecurityGroup]

EFSMountTargetAZB:
  Type: AWS::EFS::MountTarget
  Properties:
    FileSystemId: !Ref EFSFileSystem
    SubnetId: !Ref PrivateSubnetAZB
    SecurityGroups: [!Ref EFSSecurityGroup]

EFSSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: EFS mount target access
    VpcId: !Ref VPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 2049
        ToPort: 2049
        SourceSecurityGroupId: !Ref FargateTaskSecurityGroup

For teams using AWS CDK (TypeScript), the higher-level aws-efs construct library handles mount target creation automatically when you call fileSystem.connections.allowDefaultPortFrom():

import * as ecs  from 'aws-cdk-lib/aws-ecs';
import * as efs  from 'aws-cdk-lib/aws-efs';
import * as ec2  from 'aws-cdk-lib/aws-ec2';

// EFS file system — CDK creates mount targets in all VPC private subnets
const fileSystem = new efs.FileSystem(this, 'SharedFS', {
  vpc,
  encrypted: true,
  performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
  removalPolicy: cdk.RemovalPolicy.RETAIN,
});

// Allow Fargate tasks to connect on port 2049
fileSystem.connections.allowDefaultPortFrom(taskSG);

// Register the EFS volume in the task definition
taskDef.addVolume({
  name: 'shared-data',
  efsVolumeConfiguration: {
    fileSystemId: fileSystem.fileSystemId,
    transitEncryption: 'ENABLED',
    authorizationConfig: {
      accessPointId: accessPoint.accessPointId,
      iam: 'ENABLED',
    },
  },
});

// Mount inside the container
container.addMountPoints({
  sourceVolume: 'shared-data',
  containerPath: '/mnt/shared',
  readOnly: false,
});

Security: Encryption and IAM Authorization

EFS supports two dimensions of encryption and an IAM-based access control layer — all three should be enabled for any production workload.

Encryption at rest is toggled at file system creation with "Encrypted": true. EFS uses an AWS KMS key to protect data written to disk. You can supply a customer-managed key (CMK) via KmsKeyId for auditability and key rotation control, or accept the AWS-managed default.

Encryption in transit is controlled at the volume mount point in the task definition via "transitEncryption": "ENABLED". This forces the EFS client to use TLS 1.2 for the NFS connection between the Fargate task and the mount target — critical for workloads handling PII, PHI, or PCI data.

IAM authorization is layered on top of network-level access. When "iam": "ENABLED" is set in the authorizationConfig, EFS validates that the task's IAM role has the elasticfilesystem:ClientMount and elasticfilesystem:ClientWrite actions. Add this to your task role policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EFSMountAndWrite",
      "Effect": "Allow",
      "Action": [
        "elasticfilesystem:ClientMount",
        "elasticfilesystem:ClientWrite",
        "elasticfilesystem:ClientRootAccess"  // only if needed
      ],
      "Resource": "arn:aws:elasticfilesystem:us-east-1:123456789012:file-system/fs-XXXXXXXX",
      "Condition": {
        "StringEquals": {
          "elasticfilesystem:AccessPointArn":
            "arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-XXXXXXXX"
        }
      }
    }
  ]
}

Security Best PracticeAlways combine IAM authorization with EFS resource-based policies (elasticfilesystem:ClientMount with a Condition on the access point ARN). This provides defense in depth: even if network controls are misconfigured, a task without the correct IAM role cannot mount the filesystem.

Performance Modes and Throughput

EFS performance characteristics are a common source of surprise in production. Choosing the right mode at creation time matters — some settings cannot be changed after the fact.

Common Pitfalls

1. Missing mount target in the task’s AZ

If a Fargate task is placed in AZ-C but you only created mount targets for AZ-A and AZ-B, the NFS connection will fail at task startup. Always create one mount target per AZ you’re deploying tasks into. ECS will not validate this at service creation time — the failure surfaces as a task stuck in PENDING.

2. Security group port 2049 not open

The EFS mount target security group must allow inbound TCP 2049 from the Fargate task security group (not a CIDR range — use a security group reference for least privilege). Forgetting this is the single most common NFS connectivity failure in new Fargate/EFS deployments.

3. Confusing the execution role and the task role

The executionRoleArn is used by the ECS agent to pull images and write logs — it does not grant the container process any permissions. The taskRoleArn is the identity the container application assumes at runtime. EFS IAM authorization is checked against the task role, not the execution role. Attaching the EFS policy to the execution role is a very common mistake.

4. Not enabling transit encryption for regulated workloads

transitEncryption: DISABLED is the default. NFS traffic between your Fargate task and the mount target traverses the VPC unencrypted unless you explicitly set ENABLED. For HIPAA, PCI DSS, and SOC 2 workloads, in-transit encryption is typically a hard compliance requirement.

5. Treating EFS like local SSD storage

EFS latency is typically 1–3 ms per operation over NFS, orders of magnitude higher than local NVMe. Write-heavy workloads that issue thousands of small random writes (e.g., SQLite, certain logging frameworks) will be severely bottlenecked. Evaluate whether your application’s I/O pattern is EFS-compatible before committing to the architecture.

6. Sharing one EFS filesystem across microservices without access points

Multiple services writing to the same EFS root directory will inevitably collide on path names and POSIX permissions. EFS Access Points enforce an isolated directory tree and POSIX identity per service — use them even for small systems, as retrofitting them into a running filesystem is operationally painful.

Conclusion

The combination of ECS Fargate and Amazon EFS represents one of AWS’s cleanest integrations. Fargate removes the operational burden of managing EC2 infrastructure; EFS removes the burden of managing a distributed filesystem. Together, they let you build stateful containerized workloads that span Availability Zones without writing a single line of storage coordination code.

The pattern is not universally applicable — workloads with extreme IOPS requirements or millisecond-sensitive write paths should evaluate Amazon EBS on EC2 or in-memory caching layers instead. But for the broad class of applications that need shared configuration, shared uploads, or shared runtime state across a horizontally-scaled container fleet, EFS is the architecturally sound, operationally simple, and exam-correct answer.

You support me by clapping the articles you like, which encourages me to provide more content. Follow me for more AWS DevOps articles!

https://help.medium.com/hc/en-us/articles/115011350967-About-claps

https://help.medium.com/hc/en-us/articles/115011350967-About-claps


메타데이터
post_id
ebb59772b689
slug
persistent-storage-for-ecs-fargate-why-amazon-efs-is-the-cross-az-answer-ebb59772b689
url
https://awstip.com/persistent-storage-for-ecs-fargate-why-amazon-efs-is-the-cross-az-answer-ebb59772b689
canonical_url
https://awstip.com/persistent-storage-for-ecs-fargate-why-amazon-efs-is-the-cross-az-answer-ebb59772b689
author_url
https://medium.com/@sn.osmanalp
status
ok
fetched_at
2026-06-12 22:02:08