Elastic File System (EFS) persistent volumes are crucial for stateful applications on AWS, and integrating them with ECS Fargate tasks provides a powerful, serverless way to manage that state.

Here’s a look at how it works, starting with a surprising fact: the primary mechanism for EFS access in Fargate isn’t a direct mount from the Fargate task to the EFS filesystem in the traditional sense. Instead, it leverages AWS’s networking infrastructure and the EFS mount helper.

Let’s see an EFS volume in action with an ECS Fargate task. Imagine a simple Node.js application that needs to store user uploads.

// app.js
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');

const app = express();
const port = 3000;

// EFS mount point within the container
const uploadDir = '/mnt/efs/uploads';

// Ensure the directory exists
if (!fs.existsSync(uploadDir)) {
    fs.mkdirSync(uploadDir, { recursive: true });
}

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, uploadDir);
    },
    filename: function (req, file, cb) {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
    }
});

const upload = multer({ storage: storage });

app.post('/upload', upload.single('myFile'), (req, res) => {
    console.log(`File uploaded to: ${req.file.path}`);
    res.send(`File uploaded successfully to ${req.file.path}`);
});

app.get('/files', (req, res) => {
    fs.readdir(uploadDir, (err, files) => {
        if (err) {
            console.error("Error reading directory:", err);
            return res.status(500).send("Error listing files.");
        }
        res.json(files);
    });
});

app.listen(port, () => {
    console.log(`App listening at http://localhost:${port}`);
    console.log(`EFS mount point: ${uploadDir}`);
});

To run this, you’d package it into a Docker image. The crucial part for EFS integration is in the ECS Task Definition.

Here’s a snippet of what that Task Definition looks like, focusing on the volumes and mountPoints sections:

{
    "family": "my-efs-app",
    "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "1024",
    "memory": "2048",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "volumes": [
        {
            "name": "my-efs-volume",
            "efsVolumeConfiguration": {
                "fileSystemId": "fs-0123456789abcdef0",
                "rootDirectory": "/",
                "transitEncryption": "ENABLED",
                "authorizationConfig": {
                    "accessPointId": "ap-0123456789abcdef0",
                    "iam": "ENABLED"
                }
            }
        }
    ],
    "containerDefinitions": [
        {
            "name": "app-container",
            "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-efs-app:latest",
            "portMappings": [
                {
                    "containerPort": 3000,
                    "protocol": "tcp"
                }
            ],
            "mountPoints": [
                {
                    "sourceVolume": "my-efs-volume",
                    "containerPath": "/mnt/efs",
                    "readOnly": false
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/my-efs-app",
                    "awslogs-region": "us-east-1",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ]
}

Let’s break down the mental model.

The volumes section in the Task Definition is where you declare your EFS filesystem. You provide the fileSystemId of your EFS. The rootDirectory specifies a subdirectory within your EFS to mount; using / means the entire filesystem is accessible. transitEncryption should be ENABLED for security.

The authorizationConfig is critical. Using an EFS Access Point (accessPointId) is highly recommended. Access Points provide a consistent application view of an EFS file system, enforcing a specific user and group ID for all access requests originating from that access point. This simplifies permissions management within your containers. Setting iam to ENABLED means that IAM policies will be used for authorization, which is the modern and secure way to control EFS access. You’ll need to ensure your ECS task execution role has permissions to elasticfilesystem:ClientMount and elasticfilesystem:ClientWrite on your EFS filesystem and access point.

The mountPoints section within the containerDefinitions links the declared EFS volume to a specific path inside your container (containerPath). This is where your application code will see and interact with the EFS filesystem. When your Fargate task starts, AWS handles the underlying mounting process using the EFS mount helper. It establishes a secure connection from your Fargate task’s VPC network interface to your EFS filesystem.

The core magic happens because Fargate tasks run within a VPC. EFS integrates with VPCs by providing mount targets in specific subnets. When you configure your EFS filesystem, you ensure it has mount targets in the same VPC and subnets that your Fargate tasks will use. Security Groups are vital here: the Security Group attached to your Fargate task’s network interface must allow outbound NFS traffic (TCP/UDP port 2049) to the Security Group associated with your EFS mount targets. Conversely, the EFS mount target Security Group must allow inbound NFS traffic from the Fargate task’s Security Group.

The accessPointId is particularly useful because it allows you to enforce specific POSIX permissions. If you create an access point and set its PosixUser and SecondaryGids (e.g., user ID 1000, group ID 1000), and then set your container’s user to match these IDs, your application will have predictable file ownership and permissions within the EFS volume, regardless of the user running the container. Without an access point, the user ID and group ID used by the container would be what EFS sees, potentially leading to permission issues if they don’t align with the EFS filesystem’s existing ownership.

One subtle but important detail is how Fargate handles the EFS mount. It’s not a direct kernel-level mount initiated by your container. Instead, the EFS mount helper, which is baked into the Fargate environment, uses the NFS protocol over the network to access EFS. This means that while it looks like a local mount to your application, the latency and behavior are network-bound. You need to ensure your VPC has appropriate routing to reach the EFS mount targets. This usually means placing your Fargate tasks in public subnets (if they need internet access for other reasons) or private subnets with a NAT Gateway or VPC Endpoint for EFS if they don’t.

The next hurdle you’ll likely encounter is managing the lifecycle of files within EFS, especially when dealing with frequent writes or large datasets, which can lead to performance tuning and cost optimization considerations.

Want structured learning?

Take the full Ecs course →