Durable storage volume for AWS Fargate, using Cloud Development Kit (CDK)
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:
- 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.
- Amazon Elastic Container Service orchestrates attaching a durable storage volume to both containers, at the path
/usr/share/nginx/html
. - Both containers now see and share the same
index.html
file. Changes to the file are automatically propagated to both containers. - 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:
- Node.js and NPM (TypeScript will be automatically installed via
package.json
) - AWS CLI and the Session Manager plugin for AWS CLI.
Once all tools are installed, create the following files:
- package.json
- tsconfig.json
- cdk.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:
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:
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:
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:
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:
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
- If you prefer to use AWS CloudFormation directly then check out the accompanying CloudFormation pattern for an Amazon ECS task with attached Elastic File System
- If you like to work on the command line then see the tutorial for using AWS Copilot to deploy an Elastic File System backed application