Using ECS Service Extensions to attach a file system volume to a task
A service extension that attaches an Elastic File System (EFS) volume to a container running through ECS
Nathan Peck
Senior Developer Advocate at AWS
About
The ecs-service-extensions package is an extendable plugin system for defining Amazon ECS service deployments in AWS Cloud Development Kit (CDK).
Amazon ECS has a large configuration area, and many different features that can be configured. The goal of ECS Service Extensions is to make smaller, reusable chunks of declarative CDK configuration that can be applied to your service in layers.
This pattern shows a service extension that attaches an Amazon Elastic File System to a task. It configures the volume on both the container as well as the task, and also provisions the appropriate IAM permissions and security group rules to allow communication between the
file system and the container.
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.
Dependencies
To use this pattern you will need:
Node.js and NPM (TypeScript will be automatically installed via package.json)
To use this pattern you need Node.js installed. First, ensure that you have Node.js installed on your development machine. Then create the following files:
{"name":"ecs-service-extensions-cdk-efs","version":"1.0.0","description":"A container application with Elastic File System attached","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","ts-node":"^10.9.1"},"dependencies":{"aws-cdk-lib":"2.102.0","constructs":"^10.0.0","@aws-cdk-containers/ecs-service-extensions":"2.0.1-alpha.183"}}
importecs=require('aws-cdk-lib/aws-ecs');importiam=require('aws-cdk-lib/aws-iam');importefs=require('aws-cdk-lib/aws-efs');import{Service,ServiceExtension}from'@aws-cdk-containers/ecs-service-extensions';import{Construct}from'constructs';exportinterfaceVolumeProperties{path: string,readonly:boolean}// Attach a durable volume to a task, with the IAM permissions and
// security group rules that allow the filesystem to be used
exportclassDurableVolumeextendsServiceExtension{privatefilesystem: efs.FileSystem;privatepath: string;privatereadonly:boolean;constructor(props: VolumeProperties){super('durable-volume');this.path=props.path;this.readonly=props.readonly;}publicprehook(parentService: Service,scope: Construct){this.filesystem=newefs.FileSystem(scope,`${parentService.id}-file-system`,{vpc: parentService.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
});}publicuseTaskDefinition(taskDefinition: ecs.TaskDefinition):void{taskDefinition.addVolume({name:'durable-volume',efsVolumeConfiguration:{fileSystemId: this.filesystem.fileSystemId,rootDirectory:'/',transitEncryption:'ENABLED'}})// Add a policy to the task definition allowing it to point the Elastic File System
constefsMountPolicy=(newiam.PolicyStatement({actions:['elasticfilesystem:ClientMount','elasticfilesystem:ClientWrite','elasticfilesystem:ClientRootAccess'],resources:[this.filesystem.fileSystemArn]}))taskDefinition.addToTaskRolePolicy(efsMountPolicy)constappContainer=taskDefinition.findContainer('app');if(!appContainer){thrownewError('Can not add a volume to a task before adding the application container');}appContainer.addMountPoints({containerPath: this.path,readOnly: this.readonly,sourceVolume:'durable-volume'})}publicuseService(service: ecs.Ec2Service|ecs.FargateService):void{// Ensure that the service has access to communicate to the filesystem.
this.filesystem.connections.allowDefaultPortFrom(service);}}
import{ServiceExtension,ServiceBuild}from'@aws-cdk-containers/ecs-service-extensions';exportinterfaceScalingProperties{desiredCount: number}// Scales out a service to a static desired count
exportclassStaticScaleOutextendsServiceExtension{privatedesiredCount: number;constructor(props: ScalingProperties){super('static-scale');this.desiredCount=props.desiredCount;}publicmodifyServiceProps(props: ServiceBuild):ServiceBuild{return{...props,desiredCount: this.desiredCount}asServiceBuild;}}
File: ecs-exec.tsLanguage: ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import{ServiceExtension,ServiceBuild}from'@aws-cdk-containers/ecs-service-extensions';// Enables the ECS Exec feature on an ECS Service
exportclassExecextendsServiceExtension{constructor(){super('ecs-exec');}publicmodifyServiceProps(props: ServiceBuild):ServiceBuild{return{...props,enableExecuteCommand: true}asServiceBuild;}}
These extensions serve the following purpose:
DurableVolume - This extension configures an Elastic File System and attaches it to the ECS task
StaticScale - This extension scales the service to a static size of two deployed tasks
Exec - This extension enables ECS Exec so that we can open an interactive shell to a container in a task
Create the CDK App
Now create the following file to define the basic CDK application:
importecs=require('aws-cdk-lib/aws-ecs');importcdk=require('aws-cdk-lib');import{Container,Environment,HttpLoadBalancerExtension,Service,ServiceDescription}from'@aws-cdk-containers/ecs-service-extensions';import{DurableVolume}from'./efs-volume';import{StaticScaleOut}from'./static-scale';import{Exec}from'./ecs-exec';constapp=newcdk.App();conststack=newcdk.Stack(app,'efs-sample');// Create an environment to deploy a service in.
constenvironment=newEnvironment(stack,'production');// Build out the service description
constdescription=newServiceDescription();// Define the container for the service.
description.add(newContainer({cpu: 1024,memoryMiB: 2048,trafficPort: 80,image: ecs.ContainerImage.fromRegistry("public.ecr.aws/ecs-sample-image/amazon-ecs-sample"),}));// Create a load balancer and attach it to the
// container's traffic port.
description.add(newHttpLoadBalancerExtension());description.add(newDurableVolume({path:'/srv',readonly:false}));description.add(newStaticScaleOut({desiredCount: 2}))description.add(newExec())// Use the service description to make a service
// inside of the environment.
newService(stack,'ecs-sample',{environment: environment,serviceDescription: description,});app.synth();
This file attaches the service extensions to a ServiceDescription and launches it into a Service running inside of an Environment.
Deploy it all
Once you have all the files setup, it is time to deploy them. Use the following commands to preview and then deploy the CDK application:
npx cdk diff
npx cdk deploy
Once the CDK deploy completes you will see CDK output a URL that looks similar to this:
You can load this URL up in your browser to verify that the application is running. All you should see at this point is an Amazon ECS logo though.
Hydrate the durable storage volume
Right now the durable storage volume is attached to /srv inside of the container and it is empty. Let’s fix that.
Run the following command locally to open a shell to an running instance of the container. Note that you will need to open up the AWS ECS console to locate the cluster name and the ID of a running task from the service:
Once the shell opens you can start to run commands inside of the remote container. Use the following commands to create an index.html file inside of the /srv folder that is the durable filesystem volume:
cd /srv
echo"Hello world" > index.html
If you would like to, you can open a second shell to the second container now and verify that the content has been synced over to the second container.
Move the volume into place
Now that the durable volume is hydrated with some content, we can move it into place inside of the image so that you see that content when you load up the URL of the service. Update index.ts by changing the DurableStorage extension configuration to look like this:
This is redeploying the service, with the persistent storage volume attached to the web server hosting path. When the deployment completes you can load up the URL of the service in your browser, refresh once or twice to clear the browser cache, and you will see a “Hello world” message instead of the Amazon ECS logo.
At this point you can do live edits to the contents EFS volume and those changes will sync to the web server instances automatically. You can also stop and restart the web server instances and your changes to the durable volume will be persisted.
Tear it down
When you are done experimenting you can destroy the created resources with the following command:
npx 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.
🎓
New Workshop Series!
Join our upcoming container workshop series and learn best practices for Amazon ECS, AWS Fargate, and more.