Add durable storage to an ECS task, with Amazon Elastic File System

CloudFormation template showing how to mount an Elastic File System to a path inside of a container.

Nathan Peck
Nathan Peck
Senior Developer Advocate at AWS

About

In this example you will deploy two NGINX web server tasks that have a shared durable web content folder stored on an Elastic File System. You will also use Amazon ECS Exec to access the containers and verify that data is synced across tasks and persisted across task restarts.

Setup your environment

Deploy the CloudFormation

File: ecs-mount-efs-storage.yml Language: yml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::LanguageExtensions
Description: An example of how to provision an Elastic File System and
             mount it to an ECS task running in AWS Fargate

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: The virtual private network into which to launch all resources
  SubnetOne:
    Type: AWS::EC2::Subnet::Id
    Description: The first subnet across which to distribute the application and
                 EFS access
  SubnetTwo:
    Type: AWS::EC2::Subnet::Id
    Description: The second subnet across which to distribute the application and
                 EFS access

Resources:

  # The ECS cluster that will be controlling the tasks in AWS Fargate
  Cluster:
    Type: AWS::ECS::Cluster

  # The filesystem itself
  EFSFileSystem:
    Type: AWS::EFS::FileSystem
    Properties:
      Encrypted: true
      PerformanceMode: generalPurpose
      ThroughputMode: bursting

  # Mount target allows usage of the EFS inside of subnet one
  EFSMountTargetOne:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EFSFileSystem
      SubnetId: !Ref SubnetOne
      SecurityGroups:
        - !Ref EFSFileSystemSecurityGroup

  # Mount target allows usage of the EFS inside of subnet two
  EFSMountTargetTwo:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EFSFileSystem
      SubnetId: !Ref SubnetTwo
      SecurityGroups:
        - !Ref EFSFileSystemSecurityGroup

  # This security group is used by the mount targets so
  # that they will allow inbound NFS connections from
  # the AWS Fargate tasks that we launch
  EFSFileSystemSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EFS file system
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 2049
          ToPort: 2049
          SourceSecurityGroupId: !Ref ServiceSecurityGroup

  # This role is used to setup the execution environment for
  # the task, in this case to connect to the Elastic File System
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: EFSAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - elasticfilesystem:ClientMount
                  - elasticfilesystem:ClientWrite
                  - elasticfilesystem:DescribeMountTargets
                  - elasticfilesystem:DescribeFileSystems
                Resource: !GetAtt EFSFileSystem.Arn

  # This role is used at runtime.
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Policies:
        - PolicyName: ExecAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Resource: '*'

  # Store the logs from the task for inspection and review
  # for up to 7 days
  EfsTaskLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

  # This task definition describes how to launch the application, and how
  # to mount the Elastic File System into the container
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: my-ecs-task
      TaskRoleArn: !GetAtt TaskRole.Arn
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      NetworkMode: awsvpc
      ContainerDefinitions:
        - Name: nginx
          Image: public.ecr.aws/nginx/nginx:latest
          Essential: true
          LinuxParameters:
            InitProcessEnabled: true
          MountPoints:
            - SourceVolume: efs-volume
              ContainerPath: /usr/share/nginx/html
          LogConfiguration:
            LogDriver: awslogs
            Options:
              mode: non-blocking
              max-buffer-size: 25m
              awslogs-group: !Ref EfsTaskLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: efs-task
          PortMappings:
            - ContainerPort: 80
              Protocol: tcp
      Volumes:
        - Name: efs-volume
          EFSVolumeConfiguration:
            FilesystemId: !Ref EFSFileSystem
            RootDirectory: /
            TransitEncryption: ENABLED
      RequiresCompatibilities:
        - FARGATE
      Cpu: '256'
      Memory: '512'

  # Security group that the task will use to run
  ServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for service
      VpcId: !Ref VpcId

  # Launch multiple copies of the task as a service
  Service:
    Type: AWS::ECS::Service
    DependsOn:
      # This ensures that the service doesn't
      # try to start tasks before the EFS filesystem is
      # actually available in the VPC
      - EFSMountTargetOne
      - EFSMountTargetTwo
    Properties:
      ServiceName: 'ecs-efs-demo'
      Cluster: !Ref Cluster
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: 2
      EnableExecuteCommand: true
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref ServiceSecurityGroup
          Subnets:
            - !Ref SubnetOne
            - !Ref SubnetTwo
      TaskDefinition: !Ref 'TaskDefinition'

This CloudFormation template provisions an Elastic File System (EFS) and mounts it to an ECS task running in AWS Fargate.

The template begins with defining parameters that will be passed into the CloudFormation stack. The stack requires the following three parameters:

  • VpcId - A virtual private cloud ID. This can be the default VPC that comes with your AWS account. Example value: vpc-79508710
  • SubnetOne - A public subnet inside of that VPC. Example value: subnet-b4676dfe
  • SubnetTwo - Another public subnet inside of that VPC. Example value: subnet-c71ebfae

💡 Tip: When using the CloudFormation web console it will suggest appropriate parameter values in the drop down on each parameter field, if you are not certain what to enter for each parameter.

The CloudFormation stack deploys the following resources:

  • Cluster: an ECS cluster that will be controlling the tasks in AWS Fargate.
  • EFSFileSystem: the EFS filesystem itself with properties including encryption, performance mode, and throughput mode.
  • EFSMountTargetOne and EFSMountTargetTwo: mount targets that allow usage of the EFS inside subnet one and subnet two, respectively.
  • EFSFileSystemSecurityGroup: a security group used by the mount targets that allows inbound NFS connections from the AWS Fargate tasks launched.
  • TaskExecutionRole: a role used to setup the execution environment for the task, including connecting to the EFS.
  • TaskRole: a role used by the containerized task at runtime.
  • EfsTaskLogGroup: a log group to store the logs from the task for up to 7 days.
  • TaskDefinition: describes how to launch the application and how to mount the Elastic File System into the container, with properties including the task role ARN, execution role ARN, network mode, container definitions, volumes, and requires compatibilities.

This CloudFormation template creates a complete infrastructure to run an application on AWS Fargate, which requires shared, durable file persistence.

Deploy the CloudFormation template by using the web console, or with the following AWS CLI command. (Substitute your own VPC details.)

aws cloudformation deploy \
  --template-file ecs-mount-efs-storage.yml \
  --stack-name test-stack \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides VpcId=vpc-79508710 SubnetOne=subnet-b4676dfe SubnetTwo=subnet-c71ebfae

Test out Elastic File System

First open an ECS Exec session to each running container using a command like this:

aws ecs execute-command \
  --cluster <insert cluster name here> \
  --task <insert task ID here> \
  --container nginx \
  --interactive \
  --command "/bin/sh"

Run the above command twice in two different terminals, and with two different task ID’s so that you have one session open to each task.

In each session type:

cd /usr/share/nginx/html
ls

This will verify that both directories are empty.

Try running the following command in both containers:

curl localhost:80

You will see a 403 Forbidden error from NGINX because there is no content in the shared HTML directory.

Run the following command in only one of the open sessions:

echo "Hello world" > index.html

In the other container’s session run ls to see that an index.html file has appeared.

Run the curl command again in both containers:

curl localhost:80

Observe that both NGINX web servers are seeing the same web content over the Elastic File System.

Test data durability

Terminate both tasks using the Amazon ECS web console, or API.

When the ECS service starts a replacement task, open a new ECS Exec session to the replacement task.

Run the following commands again in this replacement task:

cd /usr/share/nginx/html
ls
curl localhost:80

You will see that the index.html file you wrote to the Elastic File System in the previous instance of the container has been persisted and is still there in the new replacement task.

See Also