Durable storage volume for AWS Fargate, using Cloud Development Kit (CDK)

Nathan Peck profile picture
Nathan Peck
Senior Developer Advocate at AWS

About

AWS Fargate is a serverless compute for running your containers. It comes with a large ephemeral storage volume that you can use to store data you are working on. However, this ephemeral storage space is wiped when the task stops and restarts.

Amazon Elastic File System provides durable serverless file storage over the network. An Elastic File System can be shared between multiple tasks and applications, and it automatically grows and shrinks as you store additional files or delete files.

AWS Cloud Development Kit (CDK) is an SDK that lets you write infrastructure as code as declarative statements in many popular programming languages that you are already familiar with. Rather than needing to learn a new YAML format you can use your favorite toolchains to synthesize CloudFormation that deploys your architecture.

In this pattern you will use AWS Cloud Development Kit to deploy an NGINX web server that runs in AWS Fargate. The web server will serve web content out of a shared filesystem provided by Amazon Elastic File System.

Architecture

The following diagram shows the architecture of this pattern:

AWS FargateContainerPort 80ContainerPort 80ApplicationLoad BalancerAmazon Elastic File Systemindex.htmlTrafficAWS Systems ManagerECS Exec

  1. The application deployment consists of two NGINX web server containers that run as tasks in AWS Fargate. Traffic can be sent to the containers using an Application Load Balancer.
  2. Amazon Elastic Container Service orchestrates attaching a durable storage volume to both containers, at the path /usr/share/nginx/html.
  3. Both containers now see and share the same index.html file. Changes to the file are automatically propagated to both containers.
  4. We can use the Amazon ECS Exec feature to open a secure shell to a running container and change the contents of index.html, then see the changes propagate to all tasks.

Development Environment

To use this pattern you need:

Once all tools are installed, create the following files:

  • package.json
  • tsconfig.json
  • cdk.json
File: package.jsonLanguage: json
{
  "name": "fargate-service-with-efs",
  "version": "1.0.0",
  "description": "Running a load balanced service on ECS with EFS",
  "private": true,
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "cdk": "cdk"
  },
  "author": {
    "name": "Amazon Web Services",
    "url": "https://aws.amazon.com",
    "organization": true
  },
  "license": "Apache-2.0",
  "devDependencies": {
    "@types/node": "^8.10.38",
    "aws-cdk": "2.102.0",
    "typescript": "~4.6.0"
  },
  "dependencies": {
    "aws-cdk-lib": "2.102.0",
    "constructs": "^10.0.0"
  }
}

The files above serve the following purpose:

  • package.json - This file is used by NPM or Yarn to identify and install all the required dependencies:
  • tsconfig.json - Configures the TypeScript settings for the project:
  • cdk.json - Tells CDK what command to run, and provides a place to pass other contextual settings to CDK.

CDK Application​

Now you can create an index.ts file that has the actual code for the CDK application:

File: index.tsLanguage: ts
import ecs = require('aws-cdk-lib/aws-ecs');
import ec2 = require('aws-cdk-lib/aws-ec2');
import efs = require('aws-cdk-lib/aws-efs');
import iam = require('aws-cdk-lib/aws-iam');
import elbv2 = require('aws-cdk-lib/aws-elasticloadbalancingv2');
import cdk = require('aws-cdk-lib');

const app = new cdk.App();
const stack = new cdk.Stack(app, 'fargate-efs-sample');

// Create a cluster
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 });

const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
cluster.addCapacity('DefaultAutoScalingGroup', {
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO)
});

// Define a filesystem to hold durable content
const fileSystem = new efs.FileSystem(stack, 'Filesystem', {
  vpc,
  lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, // files are not transitioned to infrequent access (IA) storage by default
  performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, // default
  outOfInfrequentAccessPolicy: efs.OutOfInfrequentAccessPolicy.AFTER_1_ACCESS, // files are not transitioned back from (infrequent access) IA to primary storage by default
});

// Create Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');
const container = taskDefinition.addContainer('nginx', {
  image: ecs.ContainerImage.fromRegistry("public.ecr.aws/nginx/nginx"),
  memoryLimitMiB: 256,
  logging: new ecs.AwsLogDriver({
    streamPrefix: 'nginx'
  })
});

container.addPortMappings({
  containerPort: 80,
  protocol: ecs.Protocol.TCP
});

// Add the Elastic File System to the task
taskDefinition.addVolume({
  name: 'web-content',
  efsVolumeConfiguration: {
    fileSystemId: fileSystem.fileSystemId,
    rootDirectory: '/',
    transitEncryption: 'ENABLED'
  }
})

// Add a policy to the task definition allowing it to point the Elastic File System
const efsMountPolicy = (new iam.PolicyStatement({
  actions: [
    'elasticfilesystem:ClientMount',
    'elasticfilesystem:ClientWrite',
    'elasticfilesystem:ClientRootAccess'
  ],
  resources: [
    fileSystem.fileSystemArn
  ]
}))
taskDefinition.addToTaskRolePolicy(efsMountPolicy)

// And add the task's filesystem to the container
container.addMountPoints({
  containerPath: '/usr/share/nginx/html',
  readOnly: false,
  sourceVolume: 'web-content'
})

// Create Service
const service = new ecs.FargateService(stack, "Service", {
  cluster,
  taskDefinition,
  desiredCount: 2,
  enableExecuteCommand: true
});

// Ensure that the service has access to communicate to the filesystem.
fileSystem.connections.allowDefaultPortFrom(service);

// Create ALB
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', {
  vpc,
  internetFacing: true
});
const listener = lb.addListener('PublicListener', { port: 80, open: true });

// Attach ALB to ECS Service
listener.addTargets('ECS', {
  port: 80,
  targets: [service.loadBalancerTarget({
    containerName: 'nginx',
    containerPort: 80
  })],
  healthCheck: {
    // For the purpose of this demo app we allow 403 as a healthy status
    // code because the NGINX webserver will initially respond with 403
    // until we put content into the Elastic File System
    healthyHttpCodes: "200,404",
    interval: cdk.Duration.seconds(60),
    path: "/health",
    timeout: cdk.Duration.seconds(5),
  }
});

new cdk.CfnOutput(stack, 'LoadBalancerDNS', { value: lb.loadBalancerDnsName, });

app.synth();

You can now use the following commands to preview infrastruture to be created, and then deploy the CDK application:

Language: shell
npm run-script cdk diff
npm run-script cdk deploy

Test out the application

The CDK application will output the URL of the load balancer in front of the application. You can also find the URL in the Amazon ECS console by locating the deployed service and looking at its details.

If you open this URL in your browser you will currently just see a 403 Forbidden error as the NGINX webserver is not going to find any content inside of the file system. Let's fix this.

Run the following command to open up an interactive shell inside one of the running containers. Note that you will have to substitute the generated name of your own ECS cluster and task ID:

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

TIP

If you get an error that says SessionManagerPlugin is not found, please install the Session Manager plugin for AWS CLI.

Inside of the shell run the following commands to create a hello world message for the NGINX webserver to respond with:

Language: shell
cd /usr/share/nginx/html
echo "Hello world" > index.html

Now you can reload the URL of the service's load balancer and see a "Hello world" message instead of 403 Forbidden. Reload the URL a few times. Even though there are two copies of the NGINX webserver you will notice that as you reload you see "Hello world" in every response. This is because the Elastic File System ensures that both running tasks have the same content available in their shared storage volume. So the file that you added on one container is accessible in the other container as well.

To ensure that your changes are durably persisted try using the Amazon ECS console to stop both tasks. Amazon ECS will restart the tasks, and when they restart you can refresh the URL again. You will still see th same "Hello world" message because the index.html file has been durably persisted into the Elastic File System, and will still be there even after the tasks stop and restart.

Tear it down

Once you are done testing you can clean up everything by running the following command:

Language: shell
npm run-script cdk destroy

WARNING

After the CDK destroy has completed you must go to the Amazon EFS console to delete the elastic filesystem. By default, CDK does not destroy filesystems or other stores of durable state, in order to avoid accidental data loss.

See Also

Alternative Patterns

Not quite right for you? Try another way to do this:

AWS Copilot CLI  Launch a task with durable storage, using AWS Copilot

AWS Copilot is a command line tool for developers that want to go from Dockerfile to deployment without touching infrastructure.

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

AWS CloudFormation is a YAML format for describing infrastructure as code.

AWS Cloud Development Kit (CDK)  Using ECS Service Extensions to attach a file system volume to a task

ECS Service Extensions enables smaller, reusable extensions for common configurations such as attaching a durable file system volume.